Thymeleafで入力エラー後にFormの値が消える原因を解説する記事のアイキャッチ。Java道場風の空間で若手エンジニアがFormとBindingResultを確認するイラスト。

Spring

Thymeleafで入力エラー後にFormの値が消える原因|th:fieldとBindingResultを確認

AIによる要約

Thymeleafで入力エラー後に値が消えるときは、th:objectth:field@Validの直後にあるBindingResult、エラー時にredirectしていないかを確認します。テキスト値が残っていても、プルダウンの選択肢一覧を再設定していないため画面が崩れることもあります。まずは「Form」「エラー情報」「画面表示用データ」の3つが再表示時に揃っているかを切り分けます。

新人SE
新人SE
入力チェックエラーを表示したら、利用者が入力した値まで空になりました。どこを見ればいいですか?
ポンコツSE
ポンコツSE
Formの受け取り方だけでなく、エラー時のreturn、th:field、選択肢の再設定を順番に確認しましょう。

登録画面で入力エラーが起きたとき、利用者が入力した内容は残したまま、該当項目の近くにエラーメッセージを表示するのが一般的です。ところが、Controllerから画面へ戻した途端に値が消えたり、プルダウンだけ空になったりすることがあります。

この不具合は、Thymeleafだけを見ても解決できません。Spring MVCがFormとBindingResultをどうModelへ入れ、テンプレートが何を参照するかを一連で確認する必要があります。この記事では社員登録画面を例に、現場での確認順を説明します。

この記事のポイント

  • 入力欄はth:objectth:fieldでFormへ結び付ける
  • BindingResultは検証対象Formの直後へ置く
  • 入力エラー時は同じテンプレートをreturnし、安易にredirectしない
  • プルダウンなどの表示用データは、エラー時にも再設定する

まず正常なForm再表示の形を確認する

入力値を保持するには、Controllerが受け取ったFormをModelに残し、Thymeleafが同じFormを参照する必要があります。基本形は次のとおりです。

public class EmployeeForm {
    @NotBlank
    private String name;

    @NotBlank
    private String departmentCode;

    @Email
    private String email;

    // getter / setter
}
@Controller
@RequestMapping("/employees")
public class EmployeeController {

    @GetMapping("/new")
    public String showForm(Model model) {
        model.addAttribute("employeeForm", new EmployeeForm());
        model.addAttribute("departments", departmentService.findAll());
        return "employees/form";
    }

    @PostMapping
    public String create(
            @Valid @ModelAttribute("employeeForm") EmployeeForm form,
            BindingResult bindingResult,
            Model model) {

        if (bindingResult.hasErrors()) {
            model.addAttribute("departments", departmentService.findAll());
            return "employees/form";
        }

        employeeService.create(form);
        return "redirect:/employees/complete";
    }
}

エラー時には受け取ったemployeeFormと、その検証結果であるBindingResultがModelに残ります。テンプレートへ直接returnすれば、利用者の入力値を使って再描画できます。

原因1: th:objectとth:fieldがForm名に合っていない

ControllerがemployeeFormという名前でFormを渡しているなら、HTML側も同じ名前を参照します。

<form th:action="@{/employees}" th:object="${employeeForm}" method="post">
    <label for="name">氏名</label>
    <input id="name" type="text" th:field="*{name}">
    <p th:if="${#fields.hasErrors('name')}"
       th:errors="*{name}"></p>

    <label for="email">メールアドレス</label>
    <input id="email" type="email" th:field="*{email}">
    <p th:errors="*{email}"></p>

    <button type="submit">登録</button>
</form>

th:field="*{name}"は、th:objectで指定したFormのnameプロパティと入力欄を結び付けます。手書きのname属性とth:valueを混在させると、エラー時にどの値が優先されるのか分かりにくくなります。

<!-- 避けたい例: Form名と参照名が一致していない -->
<form th:object="${form}">
    <input name="employeeName" th:value="${employeeForm.name}">
</form>

Controllerの@ModelAttribute名、th:objectth:fieldの3点を一続きで確認します。

原因2: BindingResultの位置が間違っている

BindingResultは、検証対象のForm引数の直後に置きます。間にModelや別の引数を挟むと、対象との対応が崩れ、期待どおりエラーを受け取れません。

// 正しい並び
public String create(
        @Valid @ModelAttribute("employeeForm") EmployeeForm form,
        BindingResult bindingResult,
        Model model) {
    // ...
}

// 間にModelが入り、BindingResultがFormの直後ではない
public String create(
        @Valid @ModelAttribute("employeeForm") EmployeeForm form,
        Model model,
        BindingResult bindingResult) {
    // ...
}

複数のFormを受け取る場合は、それぞれの直後に対応するBindingResultを置きます。引数が増えすぎたControllerは読み違いも起きやすいため、入力の責務自体を見直すことも必要です。

原因3: エラー時にredirectしている

redirectはブラウザへ別のリクエストを送らせます。そのため、POST時のFormやBindingResultは通常のModelのままでは次のGETへ引き継がれません。

if (bindingResult.hasErrors()) {
    // 入力値とエラー情報が次のGETへそのまま残らない
    return "redirect:/employees/new";
}

入力エラー時は同じテンプレートをreturn "employees/form"で再表示し、登録成功後は二重送信を防ぐためにredirectする、という使い分けが基本です。これはPost/Redirect/Get、略してPRGと呼ばれます。

場面戻り方理由
入力エラー同じテンプレートをreturnFormとBindingResultを使って再表示する
登録成功完了画面などへredirect再読み込みによる二重送信を防ぐ

RedirectAttributesのFlash Attributeで値を渡す方法もありますが、通常の入力エラー表示を複雑にしてまでredirectする必要があるかを先に考えます。特別な画面遷移要件がなければ、エラー時は直接returnする方が分かりやすい実装です。

原因4: プルダウンの選択肢を再設定していない

FormのdepartmentCodeは残っていても、選択肢のdepartmentsをModelへ再設定しなければ、プルダウンを組み立てられません。GET表示時だけ選択肢を設定している実装で起こりやすい不具合です。

<select th:field="*{departmentCode}">
    <option value="">選択してください</option>
    <option th:each="department : ${departments}"
            th:value="${department.code}"
            th:text="${department.name}">
    </option>
</select>
<p th:errors="*{departmentCode}"></p>

選択肢を設定する処理が複数のハンドラに重複するなら、privateメソッドや@ModelAttribute付きメソッドへまとめる方法があります。ただし、その画面で常に必要なデータなのかを確認して使います。

@ModelAttribute("departments")
public List<DepartmentView> departments() {
    return departmentService.findAll();
}

原因5: 新しいFormで上書きしている

エラー時に空のFormをModelへ入れ直すと、利用者が入力したFormを上書きします。

if (bindingResult.hasErrors()) {
    // 入力済みFormを空のFormで上書きしてしまう
    model.addAttribute("employeeForm", new EmployeeForm());
    return "employees/form";
}

@ModelAttributeで受け取ったFormはすでにModelへ追加されるため、通常は再登録不要です。初期表示のGETと、入力エラー時のPOSTを同じ感覚で書かないようにします。

チェックボックスとラジオボタンもth:fieldで結ぶ

<label>
    <input type="checkbox" th:field="*{agreed}">
    利用規約に同意する
</label>

<label th:each="type : ${employmentTypes}">
    <input type="radio"
           th:field="*{employmentType}"
           th:value="${type.code}">
    <span th:text="${type.name}"></span>
</label>

チェック状態を独自のth:checkedで組み立てるより、Formのプロパティへ結び付ける方が再表示時の状態を保ちやすくなります。複数選択ならForm側の型がList<String>など、画面仕様に合っているかも確認します。

原因を切り分ける確認順

  1. ブラウザのリクエストで、入力値がPOSTされているか確認する
  2. Controller受け取り直後のFormに値が入っているか確認する
  3. BindingResult#getFieldValueとエラー内容をデバッガーで確認する
  4. エラー時がredirectではなくテンプレートreturnになっているか確認する
  5. th:object名とth:fieldのプロパティ名を確認する
  6. プルダウンやラジオボタンの候補一覧がModelにあるか確認する

「Thymeleafがおかしい」と決め付けず、ブラウザ、Controller、Model、テンプレートの境界を順番に追うと、値が消えた場所を特定できます。

現場レビューでよくある指摘

// レビューコメント例
入力エラー時にredirectしているため、BindingResultが失われます。
同じテンプレートをreturnし、成功時だけredirectしてください。

// レビューコメント例
部署一覧は初期表示時だけModelへ設定されています。
エラー再表示時にも同じ一覧を設定してください。

// レビューコメント例
th:valueを個別に指定していますが、この画面はFormバインドが前提です。
th:objectとth:fieldへ統一し、再表示時の値を確認してください。

提出前のセルフチェック

  • @ModelAttribute名とth:object名が一致しているか
  • 入力項目をth:fieldでFormへ結び付けているか
  • BindingResultが検証対象Formの直後にあるか
  • 入力エラー時に同じテンプレートをreturnしているか
  • 選択肢や表示補助データを再設定しているか
  • 成功時はredirectし、二重送信を防いでいるか

Spring MVCの画面処理を整理する参考書

Form、Model、Controller、テンプレートのつながりを一度通して学ぶと、画面固有の不具合も切り分けやすくなります。Spring Bootの入門書を手元に置き、実装中の章へ戻れるようにしておくと便利です。

書籍「作って学ぶ Spring Boot入門」の表紙
商品画像:Amazon.co.jp
PR Java新人・若手向け

作って学ぶ Spring Boot入門

多田 真敏 (著)

Spring Bootを、動くWebアプリを作りながら覚える。

Controller、画面、DB連携などを一つのアプリとして実装できます。Java基礎からSpringの現場へ進む橋渡しに向く一冊です。

  • Spring Bootを手を動かして学びたい
  • Webアプリ開発の流れをつかみたい

当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。

この記事とあわせて読みたい

まとめ

Thymeleafで入力エラー後に値が消えるときは、FormだけでなくBindingResultと画面表示用データを含めて確認します。@ModelAttribute名、th:objectth:fieldを揃え、BindingResultをFormの直後へ置くのが基本です。

入力エラー時は同じテンプレートをreturnし、登録成功時にredirectします。プルダウンだけ消える場合は選択肢一覧の再設定も確認してください。データがブラウザ、Controller、Model、テンプレートのどこまで届いているかを順番に追えば、原因を切り分けられます。

-Spring
-, , , , ,