Spring Bootで@RequestParamのバリデーションが効かない原因を解説する記事のアイキャッチ。Java道場風の空間で若手エンジニアがValidationとSpring MVCを学ぶイラスト。

Spring

Spring Bootで@RequestParamのバリデーションが効かない原因|@Validatedは必要?

AIによる要約

@RequestParam@NotBlank@Minを付けても効かないときは、Validation依存関係、Springのバージョン、例外の受け取り方を確認します。Spring Framework 6.1以降のSpring MVCにはControllerメソッドの検証機能があり、旧来のAOP方式で使うクラスレベルの@Validatedを外す方が適切な構成もあります。@Validだけでは制約にならず、BindingResultも単純なRequestParam検証の受け皿ではありません。

新人SE
新人SE
@RequestParamに@NotBlankを付けたのに、空文字が通りました。@Validatedを足せば直りますか?
ポンコツSE
ポンコツSE
プロジェクトのSpringバージョンで答えが変わります。まず依存関係と実際に発生する例外を確認し、闇雲にアノテーションを足さないことが大切です。

Spring Bootの検索APIや一覧画面では、@RequestParamでキーワード、ページ番号、期間などを受け取ります。必須チェックや範囲チェックを付けたのに動かない、エラーがBindingResultへ入らない、環境を更新したら例外型が変わった、といった問題が起こりやすい箇所です。

特にWeb上には、クラスへ@Validatedを付ける古い解説と、付けない新しい解説が混在しています。この記事では、若手が現場のSpringバージョンを確認しながら判断できるよう、現在のSpring MVCと旧来方式を分けて説明します。

この記事のポイント

  • @RequestParam(required = true)@NotBlankは確認内容が違う
  • @Valid単体は「検証を連鎖させる印」で、必須制約ではない
  • Spring Framework 6.1以降はMVC標準のメソッド検証を使える
  • BindingResultは単純なRequestParam制約違反を受けるためのものではない

@RequestParamへ制約を付ける基本形

検索キーワードの空白チェックと、ページ番号の最小値チェックを行う例です。

@RestController
@RequestMapping("/api/employees")
public class EmployeeSearchController {

    @GetMapping
    public List<EmployeeResponse> search(
            @RequestParam @NotBlank @Size(max = 50) String keyword,
            @RequestParam(defaultValue = "0") @Min(0) int page) {
        return employeeService.search(keyword, page);
    }
}

ここではjakarta.validation.constraints.NotBlankなどをimportします。古いSpring Boot 2系ではjavax.validationを使うため、別世代のimportを混ぜないようにします。

requiredと@NotBlankは役割が違う

指定確認すること
@RequestParam(required = true)パラメータ自体が存在するか?keyword=は存在している
@NotNullnullではないか空文字は通る
@NotEmptynullまたは長さ0でないか半角空白だけは通る
@NotBlanknull、空文字、空白だけでないか検索語の必須チェック向き

@RequestParamrequiredは既定でtrueですが、?keyword=のようにパラメータが存在して値だけ空の場合は、存在チェックを通ることがあります。空白を拒否したいならBean Validationの制約が必要です。

「パラメータがない」と「値が空」は別のエラーとして切り分けます。

@Validだけ付けても必須チェックにはならない

// @Valid自体は「空文字禁止」という制約ではない
@GetMapping
public List<EmployeeResponse> search(
        @RequestParam @Valid String keyword) {
    return employeeService.search(keyword);
}

@Validは、FormやDTOの中にある制約を連鎖的に検証するときに使います。文字列そのものを空白禁止にしたいなら@NotBlank、長さなら@Sizeのように、確認したい条件を直接指定します。

Spring Framework 6.1以降ではMVCがメソッド検証を行う

Spring Framework 6.1以降のSpring MVCは、Controllerメソッド引数へ直接付けた@NotBlank@Minなどを検証できます。Spring Bootでは、おおむね3.2以降でこの世代のSpring Frameworkが使われます。

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @GetMapping
    public List<OrderResponse> find(
            @RequestParam @NotBlank String customerCode,
            @RequestParam @Min(1) int limit) {
        return orderService.find(customerCode, limit);
    }
}

MVC標準のメソッド検証を使う場合、旧来のAOP方式を有効にするクラスレベルの@Validatedは外すよう、Springの公式リファレンスで案内されています。Controllerをプロキシ経由で検証する方式と、MVC自身の方式を混ぜないためです。

既存Controllerから一括で@Validatedを削除してはいけません。Springのバージョン、Validationグループ、独自設定、テスト結果を確認し、プロジェクトの移行方針に従ってください。

古いSpringでは@Validatedが必要な構成がある

Spring Framework 6.1より前のControllerメソッド検証では、クラスに@Validatedを付け、Method Validationの仕組みを通す構成が広く使われていました。

@Validated
@RestController
@RequestMapping("/api/employees")
public class EmployeeSearchController {

    @GetMapping
    public List<EmployeeResponse> search(
            @RequestParam @NotBlank String keyword) {
        return employeeService.search(keyword);
    }
}

この方式では、制約違反がConstraintViolationExceptionとして扱われる構成があります。Webの記事をそのまま貼り付ける前に、pom.xmlまたはbuild.gradleでSpring Bootのバージョンを確認し、同じ世代の公式ドキュメントを見ます。

Validationの依存関係を確認する

制約アノテーションを書けても、実行に必要なValidation実装が依存関係へ入っていなければ期待どおり動きません。Spring Bootでは通常、Validation Starterを追加します。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

親プロジェクトの依存関係管理で入っている場合もあるため、重複追加する前に依存ツリーと既存モジュールを確認します。

BindingResultへ入らないのはなぜか

BindingResultは、@Valid @ModelAttributeなどでバインドしたオブジェクトの検証エラーを、Controller内で受け取るときに使います。単純な@RequestParamへ付けた制約の違反を、次のように受け取るものではありません。

// RequestParamの制約違反をBindingResultで処理する考え方ではない
@GetMapping
public List<EmployeeResponse> search(
        @RequestParam @NotBlank String keyword,
        BindingResult bindingResult) {
    // ...
}

Spring MVC 6.1以降のメソッド検証では、主にHandlerMethodValidationExceptionとして処理されます。一方、Formや@RequestBodyのオブジェクト検証ではMethodArgumentNotValidExceptionが使われる場面があります。入力方法によって例外処理を分けます。

例外ハンドラを用意する

REST APIで独自のエラー形式へ揃えるなら、@RestControllerAdviceでメソッド検証の例外を変換します。

@RestControllerAdvice
public class ApiValidationExceptionHandler {

    @ExceptionHandler(HandlerMethodValidationException.class)
    public ResponseEntity<ApiError> handleMethodValidation(
            HandlerMethodValidationException ex) {
        ApiError error = new ApiError(
                "INVALID_PARAMETER",
                "リクエストパラメータを確認してください");
        return ResponseEntity.badRequest().body(error);
    }
}

利用者向けメッセージだけでなく、どのパラメータが何の制約に違反したかをレスポンスへ含める場合は、プロジェクトの共通エラー形式へ合わせます。例外メッセージをそのまま外部へ返さないようにします。

必須不足と型変換エラーはValidationより前に起きる

パラメータ自体がない場合や、数値へ変換できない文字列が送られた場合は、Bean Validationの制約評価まで到達しないことがあります。

リクエスト例主な失敗
/api/employees必須RequestParamの不足
?page=abcStringからintへの型変換失敗
?page=-1@Min(0)の制約違反
?keyword= @NotBlankの制約違反

すべてを「Validationエラー」と一括りにせず、パラメータ不足、型変換、制約違反のどこで失敗したかをログと例外型から確認します。

パラメータが増えたらFormクラスを検討する

検索条件が2、3個なら@RequestParamでも読みやすいですが、条件が増えるとアノテーションと引数が並び、検証も複雑になります。

public class EmployeeSearchForm {
    @Size(max = 50)
    private String keyword;

    private String departmentCode;

    @Min(0)
    private int page = 0;

    @Min(1)
    @Max(100)
    private int size = 20;

    // getter / setter
}

@GetMapping
public List<EmployeeResponse> search(
        @Valid @ModelAttribute EmployeeSearchForm form,
        BindingResult bindingResult) {
    // ...
}

FormへまとめるとBindingResultでエラーを受け取りやすくなります。ただし、単一のIDを受け取るだけなのに毎回クラスを作る必要はありません。条件数、再利用性、項目間チェックの有無で判断します。

MockMvcで実際のレスポンスを確認する

@Test
void keywordが空白なら400を返す() throws Exception {
    mockMvc.perform(get("/api/employees")
                    .param("keyword", "   ")
                    .param("page", "0"))
            .andExpect(status().isBadRequest());
}

@Test
void pageが負数なら400を返す() throws Exception {
    mockMvc.perform(get("/api/employees")
                    .param("keyword", "田中")
                    .param("page", "-1"))
            .andExpect(status().isBadRequest());
}

「アノテーションが付いている」ことではなく、無効な入力を送ったときのHTTPステータスとエラー形式をテストします。バージョン更新時に例外処理が変わっても、外部仕様の変化を検知できます。

効かないときの確認順

  1. Validation Starterが依存関係へ入っているか確認する
  2. jakarta.validationjavax.validationの世代を確認する
  3. @Validではなく、必要な制約が付いているか確認する
  4. Spring BootとSpring Frameworkのバージョンを確認する
  5. クラスレベルの@Validated有無と既存方式を確認する
  6. 発生した例外型を確認し、対応するハンドラを用意する
  7. MockMvcで実際の無効入力を送って再現する

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

// レビューコメント例
@Validだけでは空文字チェックになりません。
要件に合わせて@NotBlankや@Sizeを指定してください。

// レビューコメント例
このプロジェクトはSpring MVC 6.1以降のメソッド検証を使っています。
クラスレベルの@Validatedを追加する前に、既存Controllerの方式へ揃えてください。

// レビューコメント例
RequestParamの制約違反をBindingResultで受けようとしています。
発生する例外型を確認し、共通例外ハンドラ側で処理してください。

提出前のセルフチェック

  • 存在、空白、文字数、数値範囲を区別して制約を選んだか
  • Validationの依存関係とimportの世代が合っているか
  • 利用中のSpring Frameworkバージョンを確認したか
  • @Validatedを既存方針なしに追加していないか
  • 発生する例外を共通エラー形式へ変換しているか
  • 無効な値を送るMockMvcテストがあるか

Springの検証処理を深める参考書

アノテーションの暗記ではなく、Spring MVCが引数を解決し、検証し、例外へ変換する流れを学ぶと、バージョン差にも対応しやすくなります。

書籍「Spring徹底入門 第2版」の表紙
商品画像:Amazon.co.jp
PR Springを体系的に学ぶ若手向け

Spring徹底入門 第2版

株式会社NTTデータ (著), 株式会社NTTデータグループ (編集)

Spring Frameworkの主要機能を、実務レベルで整理する。

DI、MVC、データアクセス、テスト、セキュリティまで幅広く確認できます。ピンポイント検索から体系学習へ進みたい人向けです。

  • Springの全体像を整理したい
  • 現場で使う機能を体系的に学びたい

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

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

まとめ

@RequestParamの検証が効かないときは、制約の種類、Validation依存関係、import、Springのバージョンを順番に確認します。@Validだけでは空白チェックにならず、BindingResultも単純なRequestParam制約違反の受け皿ではありません。

Spring Framework 6.1以降はMVC標準のメソッド検証が使えますが、古い案件ではクラスレベルの@Validatedを使う方式があります。検索で見つけた実装をそのまま混ぜず、プロジェクトのバージョンと既存Controllerを確認し、無効入力のテストで動作を確定させましょう。

-Spring
-, , , ,