BindingResultの使い方と@Validの直後に置く理由を解説する記事のアイキャッチ。若手エンジニアがJava道場風の学習空間で入力チェック結果を学ぶイラスト。

実装・エラー解決

BindingResultの使い方と注意点|@Validの直後に置く理由

AIによる要約

BindingResultは、@Validで検出した入力エラーをControllerで受け取るための引数です。重要なのは、@Validを付けたFormの直後に置くことです。順番を間違えると、期待通りにエラー処理できません。

新人SE
新人SE
BindingResultは何となくControllerに書いています。順番ってそんなに大事なんですか?
ポンコツSE
ポンコツSE
かなり大事です。@Valid対象の直後にBindingResultを置かないと、Springがどの入力エラーを渡せばよいか判断できず、画面に戻す前に例外になることがあります。

Spring MVCで入力チェックを実装すると、@Validと一緒にBindingResultが出てきます。

利用者登録のControllerに Model を足したところ、利用枠0を送ると入力画面へ戻らず400になったとします。同じフォーム、同じ入力で、BindingResult の順番だけが違う例を試します。

この記事のポイント

  • BindingResultは入力チェック結果を受け取るために使う
  • @Validを付けたFormの直後に置く
  • エラーがあればServiceへ進まず入力画面に戻す
  • REST APIでは例外ハンドリングに寄せる現場もある

実行用サンプル

同じ不正POSTを送り、引数の順番による200と400の違い、登録Serviceの呼び出し0回を確認します。 ソース全文、JUnitの検証コード、curlの実行ファイル、pom.xml、READMEを収録しています。JDK 17とMaven 3.9系で mvn test を実行します。Spring Boot 3.5.16のサンプルです。

BindingResultは入力チェック結果を受け取る

BindingResult は型変換などのバインドエラーと、@Valid で検出した制約違反を保持します。利用枠 abc は型変換で、整数の 0 は @Min(1) でエラーになります。

UserRegistrationController.java の抜粋。入力エラーをフォームの直後で受け取る

    @PostMapping("/users")
    public String register(@Valid @ModelAttribute("userRegisterForm") UserRegisterForm form,
                           BindingResult errors, Model model) {
        if (form.getDepartment() != null && !form.getDepartment().isBlank()
                && DEPARTMENTS.stream().noneMatch(department -> department.code().equals(form.getDepartment()))) {
            errors.rejectValue("department", "unknownDepartment", "一覧から部署を選択してください");
        }
        if (errors.hasErrors()) {
            model.addAttribute("showCorrectionNotice", true);
            return "users/new";
        }
        userService.register(form.getName(), form.getDepartment(), form.getSeatLimit());
        return "redirect:/users/new?registered";
    }

bindingResult.hasErrors()がtrueなら、入力値に問題があります。この時点でServiceへ進まず、入力画面へ戻すのが基本です。

@Validの直後に置く理由

最重要ポイントは、BindingResultを@Valid対象の直後に置くことです。Springは、直前の検証対象に対するエラー結果としてBindingResultを扱います。

上の引数は Form → BindingResult → Model の順です。画面用の Model は BindingResult の後ろに置けます。

この順番では、引数を準備する段階で検出したエラーを errors に渡し、Controllerの本体が実行されます。利用枠 0 を送ると、HTTP 200の登録画面に 0 と「利用枠は1以上で入力してください」が表示されます。

順番を間違えるとどうなるか

よくあるミスは、@ValidとBindingResultの間に別の引数を挟むことです。

UserRegistrationController.java の抜粋。引数の間にModelを挟んだ失敗例

    @PostMapping("/demo/users/wrong-order")
    public String wrongOrder(@Valid @ModelAttribute("userRegisterForm") UserRegisterForm form,
                             Model model, BindingResult errors) {
        return register(form, errors, model);
    }

間に Model を挟むと、Spring MVCはFormの直後にエラーを受け取る引数がないと判定します。利用枠 0 では、Controllerの本体が実行される前に MethodArgumentNotValidException になり、このサンプルではHTTP 400を返します。

展開したプロジェクトで mvn spring-boot:run を実行し、別のターミナルで同じディレクトリから次を試します。上が正しい順で200、下が誤った順で400です。

compare.sh の抜粋。同じPOSTで引数の順序の違いを比較

base_url="${BASE_URL:-http://localhost:8080}"

# 正しい順番はHTTP 200。利用枠0のエラーを画面に返す
curl -si "$base_url/users" \
  --data-urlencode 'name=Sato' \
  --data-urlencode 'department=OPS' \
  --data-urlencode 'seatLimit=0'

# 誤った順番はHTTP 400。Controllerの処理に入る前に失敗する
curl -si "$base_url/demo/users/wrong-order" \
  --data-urlencode 'name=Sato' \
  --data-urlencode 'department=OPS' \
  --data-urlencode 'seatLimit=0'

エラーがあればServiceへ進まない

形式チェックでエラーがある状態でServiceを呼ぶと、Service側で想定外の値を扱うことになります。Controllerでは、エラーの有無を見て処理を止めます。

UserRegistrationController.java の抜粋。エラーがある時点で登録を止める部分

        if (errors.hasErrors()) {
            model.addAttribute("showCorrectionNotice", true);
            return "users/new";
        }
        userService.register(form.getName(), form.getDepartment(), form.getSeatLimit());

同じ seatLimit=0 で、正しい順番は画面を返し、誤った順番は例外になります。どちらも userService.register の呼び出しは0回です。誤った順番でも seatLimit=2 なら302で登録に進むため、レビューでは不正値も送って確かめます。

Thymeleafでエラーを表示する

画面側では、Formのフィールドに紐づくエラーを表示します。

new.html の抜粋。利用枠の入力値とエラーを表示する部分

    <label>利用枠 <input type="text" inputmode="numeric" th:field="*{seatLimit}"></label>
    <span th:errors="*{seatLimit}"></span>

テンプレートは userRegisterForm を参照し、th:field で利用枠の入力を残し、th:errors で同じ項目のメッセージを表示します。BindingResult を正しく受けた後は、フォームを作り直さず、そのリクエストのままビューを返します。

REST APIでは扱い方が変わることもある

画面フォームではBindingResultを使って入力画面に戻す実装が分かりやすいです。一方、REST APIでは、入力エラーを例外ハンドラでまとめてJSONレスポンスにする現場もあります。

JSONを受けるAPIでは、@Valid @RequestBody の制約違反を例外として処理し、HTTP 400を返す構成があります。画面フォームを同じビューで再描画する今回の例とは、応答の役割が異なります。

エラーのJSON形式を決めるときは、APIで使う共通のエラー処理を確認します。JSONの入力チェックを切り分ける方法は、RequestBodyで受信したJSONが400になる原因で補足しています。

レビュー前のセルフチェック

確認リスト

  • Formの直後に BindingResult があり、Model を挟んでいないか
  • 利用枠0で入力とメッセージが画面に残り、登録Serviceの呼び出しが0回か
  • 画面側でForm名とフィールド名が一致しているか
  • APIと画面フォームでエラー処理を混同していないか

次は誤った順番のテストです。400、例外、フィールドの制約エラー、登録が呼ばれていないことを確認します。

UserRegistrationTest.java の抜粋。Modelを間に置いた失敗を検証

    @Test
    void modelBetweenFormAndBindingResultMakesInvalidPostFailBeforeHandlerBody() throws Exception {
        var result = mvc.perform(post("/demo/users/wrong-order").formField("name", "Sato")
                        .formField("department", "OPS").formField("seatLimit", "0"))
                .andExpect(status().isBadRequest())
                .andExpect(handler().methodName("wrongOrder"))
                .andReturn();
        assertThat(result.getResolvedException())
                .isInstanceOfSatisfying(MethodArgumentNotValidException.class, error ->
                        assertThat(error.getBindingResult().getFieldError("seatLimit").getCode()).isEqualTo("Min"));
        assertThat(service.calls).isEmpty();
    }

両記事に共通するサンプル全体のテスト結果です。

Tests run: 9, Failures: 0, Errors: 0, Skipped: 0

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

関連記事Spring Bootの@RequestParamとFormクラスの違いは?使い分けを解説 @RequestParam は単発の値を受け取るときに便利です。一方、Formクラスは画面入力や検索条件のように複数項目を1つの入力として扱いたいときに向いています。 記事を読む 関連記事Spring Bootの@Validと@Validatedの違い|必須チェックが効かない原因と使い分け Spring Bootの @Valid と @Validated は、通常の入力検証とグループ指定で使い分けます。特に「 @Validated(CreateGroup.class) へ変えたら @NotBlank が効かない」場合は、共通制約の Default グループが検証対象に含まれているかを確認してください。 記事を読む 関連記事Spring Bootで入力チェックが重複する|ControllerとServiceの分け方 先に結論画面とAPIの両方に同じ重複チェックを書いているなら、メール重複などの業務判断をServiceへ集約します。 記事を読む

まとめ

BindingResultは、@Validの結果をControllerで受け取るためのものです。最も大事なのは、@Validを付けた引数の直後に置くことです。

新人のうちは、順番、エラー時にServiceへ進まないこと、画面に戻したときにエラーが表示されること。この3点を押さえれば十分実務で使えます。

-実装・エラー解決
-,