Spring BootでValidationメッセージが表示されない原因を解説する記事のアイキャッチ。Java道場風の空間で若手エンジニアがth:errorsを確認するイラスト。

Spring

Spring Bootでバリデーションメッセージが表示されない原因|th:errorsとmessages.propertiesを確認

AIによる要約

バリデーションメッセージが画面へ出ないときは、まずBindingResultにエラーが入っているかを確認します。エラーがあるのに表示されない場合は、Formの属性名、th:objectth:fieldth:errorsの項目名を揃えます。メッセージキー、配置ファイル、リダイレクトの有無も確認し、Validationが動かない問題と画面表示だけの問題を分けて調査することが重要です。

新人SE
新人SE
空欄で登録すると同じ画面へ戻るのですが、「名前を入力してください」が表示されません。@NotBlankは付いています。
ポンコツSE
ポンコツSE
入力チェック自体が動いていないのか、エラーはあるがThymeleafが表示できていないのかを先に分けましょう。

Spring BootとThymeleafの入力画面では、Formに@NotBlankを付けただけではメッセージは自動表示されません。Controllerで検証し、BindingResultを保持したまま画面へ戻し、HTML側で対象エラーを表示する必要があります。

新人がつまずきやすいのは、アノテーション、Controller、テンプレート、メッセージファイルを一度に直してしまうことです。確認順を決めれば、問題が「検証」「受け渡し」「表示」「文言解決」のどこにあるか判断できます。

この記事のポイント

  • 最初にBindingResult#hasErrors()を確認する
  • @Valid Formの直後にBindingResultを置く
  • model属性名、th:object、th:field、th:errorsを一致させる
  • redirectするとBindingResultが通常のリクエストをまたげない
  • メッセージキーとpropertiesの配置・文字コードを確認する

まずBindingResultにエラーがあるか確認する

画面表示を直す前に、Controllerで入力チェック結果を確認します。

@PostMapping("/users")
public String create(
        @Valid @ModelAttribute("userForm") UserForm form,
        BindingResult bindingResult) {

    if (bindingResult.hasErrors()) {
        return "users/input";
    }

    userService.create(form);
    return "redirect:/users";
}

デバッガーでbindingResult.getFieldErrors()を確認し、nameなどのエラーが入っているならValidationは動いています。その場合、原因はテンプレートや属性名の可能性が高くなります。

エラーが0件なら、@Validが付いているか、対象Formに制約が付いているか、依存関係にValidationが含まれているか、送信値が本当に空かを確認します。

BindingResultは@Validの対象直後に置く

// OK
public String create(
        @Valid @ModelAttribute("userForm") UserForm form,
        BindingResult bindingResult,
        Model model) {
    // ...
}

// NG: 別の引数を間へ挟まない
public String create(
        @Valid @ModelAttribute("userForm") UserForm form,
        Model model,
        BindingResult bindingResult) {
    // ...
}

BindingResultは、直前の検証対象に対応します。FormとBindingResultの間へModelなどを挟むと、意図した検証結果を受け取れず、例外や想定外の動作につながります。

Formクラスと制約を確認する

public class UserForm {

    @NotBlank(message = "{user.name.required}")
    @Size(max = 50, message = "{user.name.max}")
    private String name;

    @Email(message = "{user.email.invalid}")
    private String email;

    // getter / setter
}

空文字を拒否したいStringには、通常@NotNullだけでは足りません。@NotNullはnullだけを拒否し、空文字は許可します。前後空白だけの入力も拒否したいなら@NotBlankを使います。

制約null空文字空白だけ
@NotNullエラー許可許可
@NotEmptyエラーエラー許可
@NotBlankエラーエラーエラー

アノテーションの選び方が違うと、表示以前にエラーが作られません。「必須」の定義を画面仕様と照合してください。

th:objectとmodel属性名を一致させる

ControllerがuserFormという名前でFormを渡すなら、Thymeleafも同じ名前を参照します。

<form th:action="@{/users}"
      th:object="${userForm}"
      method="post">
  <input type="text" th:field="*{name}">
  <p th:errors="*{name}">名前のエラー</p>
  <button type="submit">登録</button>
</form>

th:object="${userForm}"を指定した内側では、*{name}がuserForm.nameを表します。Controller側が"form"、HTML側が"userForm"では一致しません。

@GetMapping("/users/new")
public String input(Model model) {
    model.addAttribute("userForm", new UserForm());
    return "users/input";
}

初期表示でも同じ属性名を用意します。POSTエラー時だけ表示でき、GET初期表示でテンプレートエラーになる場合は、GET側のForm追加漏れを疑います。

th:errorsの項目名を確認する

<!-- フィールド単位 -->
<p th:if="${#fields.hasErrors('name')}"
   th:errors="*{name}"></p>

<!-- Form全体に付けたグローバルエラー -->
<div th:if="${#fields.hasGlobalErrors()}">
  <p th:each="error : ${#fields.globalErrors()}"
     th:text="${error}"></p>
</div>

<!-- すべてのエラーをまとめて確認 -->
<ul th:if="${#fields.hasAnyErrors()}">
  <li th:each="error : ${#fields.allErrors()}"
      th:text="${error}"></li>
</ul>

nameのエラーをemailの場所へ出そうとしても表示されません。また、パスワード確認のような複数項目チェックをグローバルエラーとして登録した場合、th:errors="*{name}"では拾えません。

messages.propertiesのキーを確認する

先ほどのFormでmessage = "{user.name.required}"と書いた場合、メッセージファイルへ対応するキーを用意します。

# src/main/resources/messages.properties
user.name.required=名前を入力してください。
user.name.max=名前は50文字以内で入力してください。
user.email.invalid=メールアドレスの形式で入力してください。

キーが見つからない場合、{user.name.required}のような文字がそのまま出たり、既定メッセージへフォールバックしたりします。ファイル名、配置場所、キーの綴りを確認します。

messages.propertiesとValidationMessages.propertiesを混同しない

Springの画面文言は通常MessageSourceのmessages.propertiesを使います。Jakarta Validation単体の既定バンドルはValidationMessages.propertiesです。Spring Boot側の設定やバージョンで解決経路が変わるため、既存プロジェクトがどのファイルへメッセージを集約しているかに揃えてください。

日本語が文字化けする場合は、IDE、ビルド、実行環境でpropertiesの文字コード設定を確認します。Spring Bootの現行構成ではUTF-8で扱えることが多いですが、古い案件や独自MessageSource設定ではISO-8859-1前提が残っていることがあります。

メッセージをアノテーションへ直書きするかキー化するか

// 小規模・一時的には分かりやすい
@NotBlank(message = "名前を入力してください。")
private String name;

// 再利用・文言統一・多言語化を考えるならキー化
@NotBlank(message = "{user.name.required}")
private String name;

直書きなら原因を追いやすい一方、同じ文言が複数Formへ散ります。業務システムでは文言レビューや表記統一があるため、プロジェクトがキー管理しているならそれに合わせます。キー化自体が目的になり、意味の分からない連番キーを増やすのは避けます。

redirectするとエラー情報が消える

@PostMapping("/users")
public String create(
        @Valid @ModelAttribute("userForm") UserForm form,
        BindingResult bindingResult) {

    if (bindingResult.hasErrors()) {
        return "redirect:/users/new"; // エラー情報を通常は引き継げない
    }

    return "users/input"; // 入力エラー時は同じリクエストで画面を返す
}

上のコメントは意図を示すための比較で、実際にはif内をreturn "users/input";にします。リダイレクトは新しいリクエストになるため、BindingResultと入力値をそのまま引き継げません。

登録成功後は二重送信を防ぐためredirect、入力エラー時は同じ画面名をreturnする、という使い分けが基本です。Flash Attributeで引き継ぐ方法もありますが、まずは単純な構成を優先します。

表示されない時の確認順

  1. BindingResultにフィールドエラーが入っているか確認する
  2. @Validの対象直後にBindingResultがあるか確認する
  3. 制約が入力仕様に合っているか確認する
  4. Controllerのmodel属性名とth:objectを合わせる
  5. th:fieldとth:errorsの項目名を合わせる
  6. フィールドエラーかグローバルエラーかを確認する
  7. 入力エラー時にredirectしていないか確認する
  8. メッセージキー、ファイル配置、文字コードを確認する

BindingResultにエラーがあるかどうかで、調査対象をJava側とHTML側へ大きく分けられます。

MockMvcでメッセージ表示まで確認する

@Test
void 名前が空なら入力画面へ戻してエラーを表示する() throws Exception {
    mockMvc.perform(post("/users")
                    .param("name", "")
                    .param("email", "test@example.com"))
            .andExpect(status().isOk())
            .andExpect(view().name("users/input"))
            .andExpect(model().attributeHasFieldErrors(
                    "userForm",
                    "name"));
}

文言そのものを完全一致で固定すると、表記修正だけでテストが大量に壊れることがあります。重要な画面ではメッセージコードや表示文言も確認し、それ以外ではエラー項目の存在を中心に確認するなど、テストの目的を分けます。

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

// レビューコメント例
BindingResultにnameのエラーはありますが、テンプレートが*{userName}を参照しています。
Formのプロパティ名とth:errorsを揃えてください。

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

// レビューコメント例
必須項目へ@NotNullだけを付けていますが、空文字を拒否できません。
画面仕様が「空白不可」なら@NotBlankが適切か確認してください。

提出前のセルフチェック

  • BindingResultに期待するエラーが入ることを確認したか
  • FormとBindingResultを隣に置いたか
  • GETとPOSTで同じmodel属性名を使っているか
  • th:object、th:field、th:errorsの名前が一致しているか
  • フィールドエラーとグローバルエラーを表示できるか
  • 入力エラー時にredirectしていないか
  • メッセージキーとpropertiesの配置を確認したか
  • 日本語の文字化けがないか実画面で確認したか

Spring Bootの画面入力を体系的に学ぶ参考書

Form、Validation、BindingResult、Thymeleafを一連の画面処理として学ぶと、メッセージ表示の原因を層ごとに切り分けやすくなります。

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

作って学ぶ Spring Boot入門

多田 真敏 (著)

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

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

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

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

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

まとめ

バリデーションメッセージが表示されないときは、最初にBindingResultへエラーが入っているか確認します。エラーがなければ制約とValidation、エラーがあればThymeleafと属性名を中心に調べます。

Form名、項目名、th:object、th:errors、メッセージキーを揃え、入力エラー時はredirectせず同じ画面を返します。確認順を固定すれば、アノテーションを増やすだけの場当たり的な修正を避けられます。

-Spring
-, , , , ,