`@Valid` を付けたフォームで入力エラーが起きたとき、「そのエラーをControllerでどう受け取り、どう画面へ返すのか」で迷いやすいのが `BindingResult` です。使い方の要点はシンプルで、検証対象の引数の直後に置き、`hasErrors()` を確認してエラー時は同じフォームへ戻します。ただし、BindingResultにはBean Validationの失敗だけでなく、型変換などデータバインド時のエラーも入ります。ここを理解すると、単に `if (result.hasErrors())` と書くより一段深く原因を追えるようになります。
まず結論:BindingResultは検証対象の直後に置く
Spring MVCでフォームオブジェクトを `@Valid` で検証し、Controller内でエラーを扱う基本形は、検証対象の直後に `BindingResult` を宣言することです。公式ドキュメントでも `Errors` または `BindingResult` は対象の引数の直後に置く必要があるとされています。
@PostMapping("/users")
public String create(
@Valid @ModelAttribute("userForm") UserForm form,
BindingResult result) {
if (result.hasErrors()) {
return "users/form";
}
userService.create(form);
return "redirect:/users";
}
| 確認ポイント | 役割 |
|---|---|
| `@Valid` | フォームオブジェクトへvalidationを適用する |
| `BindingResult` | データバインド・validationのエラー結果へアクセスする |
| `hasErrors()` | 1件以上のエラーがあるか判定する |
| エラー時に同じViewを返す | 入力値とエラー情報を使ってフォームを再表示する |
| 正常時だけServiceへ進む | 不正な入力を業務処理へ渡さない |
最初はこの形を基準にし、必要になったら個別フィールドのエラー、global error、型変換失敗まで掘り下げると整理しやすくなります。
BindingResultとは何を持っているのか
`BindingResult` はSpringの `Errors` インターフェースを拡張し、対象オブジェクトのデータバインドとvalidationの結果を表すインターフェースです。単なる「@Validのエラー一覧」ではなく、フォームへ値をバインドする途中で起きたエラーも扱えるのが重要なポイントです。
用語解説:データバインド
HTTPリクエストの文字列パラメータなどを、フォームオブジェクトのプロパティへ変換して設定する処理です。たとえば数値フィールドへ文字列が届き型変換できなければ、validationより前段のbinding errorとして記録されることがあります。
| 種類 | 例 | 確認方法 |
|---|---|---|
| validation error | `@NotBlank`、`@Email` などの制約違反 | `getFieldErrors()`、`getAllErrors()` |
| binding error | 数値項目に変換できない文字列が送信された | `FieldError#isBindingFailure()` |
| global error | 複数項目をまたぐ整合性エラーなど | `getGlobalErrors()` |
(@ValidやBean Validationの基本から確認したい場合については『Spring Bootのフォームバリデーションとは?@Validで安全な入力チェックを実現しよう』をご参照ください)
フォームバリデーションを実装する基本形
例として、名前とメールアドレスを受け取るフォームを考えます。まずフォーム専用オブジェクトに制約を定義します。
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
public class UserForm {
@NotBlank(message = "名前は必須です")
private String name;
@NotBlank(message = "メールアドレスは必須です")
@Email(message = "メールアドレスの形式で入力してください")
private String email;
// getter / setter
}
Controllerでは `@Valid` を付けた `UserForm` の直後に `BindingResult` を置きます。validationに失敗しても、BindingResultで受け取れる構成ならController内で `hasErrors()` を確認し、同じフォームViewへ戻せます。
@PostMapping("/users")
public String create(
@Valid @ModelAttribute("userForm") UserForm form,
BindingResult result) {
if (result.hasErrors()) {
return "users/form";
}
userService.create(form);
return "redirect:/users";
}
hasErrorsだけでなくエラーの中身を取り出す
画面を戻すだけなら `hasErrors()` で十分ですが、原因調査やAPIレスポンス整形などではエラーの中身を確認したくなります。`BindingResult` が継承する `Errors` には、field errorとglobal errorを取得するメソッドがあります。
| メソッド | 使いどころ |
|---|---|
| `hasErrors()` | 何らかのエラーがあるかだけ確認する |
| `getAllErrors()` | field/globalを含む全エラーを取得する |
| `getFieldErrors()` | 全field errorを取得する |
| `getFieldError("email")` | 特定フィールドの先頭エラーを取得する |
| `hasFieldErrors("email")` | 特定フィールドにエラーがあるか確認する |
| `getGlobalErrors()` | 特定フィールドに属さないオブジェクト全体のエラーを取得する |
for (FieldError error : result.getFieldErrors()) {
System.out.println(
error.getField() + " : " + error.getDefaultMessage()
);
}
ログやレスポンスへそのまま出す前に、個人情報や入力値を含まないかは確認してください。エラー情報を取得できることと、すべてを外部へ公開してよいことは別です。
画面へエラーメッセージを戻す
Thymeleafを使う場合、フォームの `th:object` と各入力の `th:field` を対応させ、`#fields.hasErrors(…)` や `th:errors` でエラーを表示できます。Controller側はエラー時にredirectせず、フォームViewをそのまま返す基本形が分かりやすいです。
<form th:action="@{/users}" th:object="${userForm}" method="post">
<input type="text" th:field="*{name}">
<p th:if="${#fields.hasErrors('name')}"
th:errors="*{name}"></p>
<input type="email" th:field="*{email}">
<p th:if="${#fields.hasErrors('email')}"
th:errors="*{email}"></p>
<button type="submit">保存</button>
</form>
SpringとThymeleafの連携では、フォームオブジェクトに紐づくエラー情報を使ってフィールド単位のメッセージを表示できます。画面側で独自に同じvalidation条件を再実装せず、サーバー側の結果を表示する形に寄せると条件の二重管理を減らせます。
binding errorとvalidation errorを分けて見る
`BindingResult` の強みは、制約違反だけでなくデータバインド失敗も同じ入口から確認できることです。たとえば `Integer age` に `abc` が送られた場合、`@Min` の判定以前に型変換で失敗する可能性があります。
for (FieldError error : result.getFieldErrors()) {
if (error.isBindingFailure()) {
// 型変換など、データバインド段階の失敗
System.out.println("binding error: " + error.getField());
} else {
// Bean Validationなどのvalidation failure
System.out.println("validation error: " + error.getField());
}
}
「@Validのメッセージが想定と違う」ときは、制約アノテーションだけを見るのではなく、そもそも対象型へ値が変換できているかも確認すると原因を切り分けやすくなります。
rejectValueとrejectで追加エラーを登録する
Bean Validationのアノテーションだけで表しにくいチェックをControllerやValidatorで行い、BindingResultへエラーを追加することもできます。フィールドに紐づけるなら `rejectValue`、オブジェクト全体へ付けるなら `reject` を使います。
if (!Objects.equals(form.getPassword(), form.getPasswordConfirm())) {
result.rejectValue(
"passwordConfirm",
"password.mismatch",
"パスワードが一致しません"
);
}
if (result.hasErrors()) {
return "users/form";
}
複雑な業務ルールをControllerへ大量に書くのではなく、責務が大きくなったら専用ValidatorやService側の検証へ分離する判断も必要です。BindingResultはエラーを保持する器であり、すべてのvalidationロジックをControllerへ集約する理由にはなりません。
(カスタムアノテーションやMVCへのvalidation統合を深掘りしたい場合については『Spring Boot バリデーション入門|MVC統合とカスタムアノテーション実践例』をご参照ください)
BindingResultが効かないときの確認順
`result.hasErrors()` が常に `false` になる、またはControllerへ入る前に例外になる場合は、次の順で確認すると切り分けやすくなります。
- `@Valid` または必要な `@Validated` が検証対象へ付いているか
- `BindingResult` が検証対象の引数の直後に置かれているか
- フォームオブジェクトに `@NotBlank` や `@Email` など目的の制約が付いているか
- 入力値がvalidation以前のデータバインドで失敗していないか
- Controllerメソッドの別パラメータに直接Constraintを付け、method validationが適用される構成になっていないか
- 画面の `th:object` / `th:field` とControllerのmodel attribute名が対応しているか
現在のSpring MVCでは、メソッド引数へ直接Constraintを付けるなどmethod validationが適用されると、エラーの扱いが `HandlerMethodValidationException` 側になるケースがあります。BindingResultだけを見て原因を決めつけないことが大切です。
(validation自体が発火しない・期待通り効かない場合については『Spring Boot バリデーション効かない?5分で直す方法』をご参照ください)
REST APIでは例外処理との使い分けを考える
`BindingResult` はフォームだけの仕組みではなく、Spring MVCのmethod argumentとして `@RequestBody` や `@RequestPart` のvalidation errorへアクセスする用途でも使えます。一方、REST APIではエラー形式を全エンドポイントで統一したいことが多いため、例外処理へ寄せる設計も選択肢になります。
| 用途 | 扱い方の例 |
|---|---|
| HTMLフォームで同じ画面へ戻す | `BindingResult` を直後に置き、Controller内でViewを分岐する |
| REST APIでControllerごとに個別処理したい | `BindingResult` を直後に置いてエラーレスポンスを組み立てる |
| REST APIでエラー形式を共通化したい | BindingResultを置かず、`MethodArgumentNotValidException` などを `@ControllerAdvice` / `@ExceptionHandler` で扱う |
| method validationが適用される | `HandlerMethodValidationException` も考慮する |
どちらが常に正解という話ではありません。画面遷移をController内で決めたいのか、APIエラーを横断的に統一したいのかで、エラーを受け取る場所を決めると設計がぶれにくくなります。
まとめ:BindingResultはエラーを次の処理へつなぐ結果ホルダー
- `BindingResult` はdata bindingとvalidationのエラー結果へアクセスするためのインターフェース
- `@Valid` などで検証する引数の直後に置くのが基本
- `hasErrors()` だけでなく `getFieldErrors()`・`getGlobalErrors()` で内容を確認できる
- `FieldError#isBindingFailure()` を見ると型変換などbinding段階の失敗を切り分けられる
- Thymeleafでは `th:field` と `th:errors` を使い、サーバー側のvalidation結果を画面へ表示できる
- REST APIではBindingResultで局所処理するか、例外を共通処理するかを設計で選ぶ
まずは現在のControllerで、検証対象と `BindingResult` の並び順を確認し、`hasErrors()` の中で `getFieldErrors()` を一度見てみてください。エラーが「制約違反」なのか「値をオブジェクトへ変換できなかった」のかまで分かると、validationトラブルの切り分けが一気に具体的になります。