markdown

はじめに (対象読者・この記事でわかること)

本記事は、Java(特にSpring Boot)でWebサービスを開発しているエンジニア、もしくはOpenAPI(Swagger)からコードを自動生成した経験がある方を対象としています。
OpenAPI の openapi.yaml から springdoc-openapi などのジェネレータでコントローラ・DTO が自動生成されたプロジェクトに対し、クエリパラメータの入力制約(必須・形式・範囲)をサーバ側で確実にチェックしたいというニーズに応える内容です。

この記事を読むことで、以下が実現できるようになります。

  • OpenAPI の parameter 定義を Java のバリデーションアノテーションへ変換する手順
  • Spring の @Validated@ValidBindingResult を使った実装パターン
  • バリデーションエラー時のカスタム例外ハンドラと統一レスポンスの作り方

背景として、フロントエンド側でのバリデーションは必須ですが、サーバ側での二重チェックが欠如すると不正データがデータベースに流入したり、予期しない例外が発生したりします。自動生成コードに手を加える際のベストプラクティスを示すことで、保守性と安全性を同時に高められます。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。

  • Java 11 以上、Spring Boot 2.7 以降の基本的な使い方
  • Maven または Gradle のビルド設定、依存関係の追加方法
  • OpenAPI(Swagger)仕様書の基本構造(paths、parameters、schemas)

OpenAPI で生成されたコードの概要とバリデーションが必要になる背景

springdoc-openapi-maven-plugin などを用いると、openapi.yamlparameters 定義から次のようなコントローラメソッドが自動生成されます。

Java
@GetMapping("/api/v1/users") public ResponseEntity<List<UserDto>> getUsers( @RequestParam(value = "page", required = false) Integer page, @RequestParam(value = "size", required = false) Integer size, @RequestParam(value = "email", required = false) String email) { // 省略 }

このままでは、以下の問題が残ります。

  1. 必須項目のチェックが自動で入らない
    OpenAPI では required: true と記述していても、生成コードは required = false になることがある。
  2. 形式・範囲の制約が失われる
    minimum, maximum, pattern などのバリデーション情報は、Java のバリデーションアノテーション (@Min, @Max, @Pattern) に変換されていない。
  3. エラーメッセージが統一されていない
    Spring の標準例外 (MethodArgumentTypeMismatchException など) がそのまま返却され、API 利用者にとっては意味不明な JSON が返る。

そこで、OpenAPI のパラメータ定義と Java バリデーションを橋渡しする仕組みを自前で用意します。主に以下の三点に注目します。

  • DTO に変換してバリデーションアノテーションを付与
    コントローラの引数を @ModelAttribute で束ねたクラスに置き換え、@NotNull, @Size, @Pattern などを付与。
  • @ValidatedBindingResult でバリデーション結果を取得
    エラーがあればカスタム例外にラップし、統一フォーマットでレスポンス。
  • コード自動生成と手動カスタマイズの分離
    生成されたコードはそのまま残し、@Generated アノテーションでマークした上に手動で追加した DTO を別ファイルに配置。再生成時に上書きされないようにする。

以下の章で、実際に手を動かしながらこのフローを構築していきます。

クエリパラメータバリデーション実装のステップバイステップ

ステップ 1. OpenAPI 定義にバリデーション情報を明示する

まずは openapi.yaml にバリデーション情報を記述します。例としてユーザー一覧取得 API のクエリパラメータを拡張します。

Yaml
paths: /api/v1/users: get: summary: ユーザー一覧取得 parameters: - name: page in: query description: 取得したいページ番号(1 起点) required: true schema: type: integer minimum: 1 - name: size in: query description: 1 ページあたりの件数 required: false schema: type: integer minimum: 1 maximum: 100 - name: email in: query description: メールアドレスで絞り込み required: false schema: type: string format: email pattern: '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$'
  • required: true → 必須チェック
  • minimum, maximum → 数値範囲
  • format: emailpattern → 正規表現による形式チェック

この情報はコード生成時に利用できるようにしておくと、後述の 自動マッピング が容易になります。

ステップ 2. 生成コードにバリデーション用 DTO を追加

自動生成されたコントローラはそのまま残し、クエリパラメータ用の DTO を手動で作成します。パッケージは com.example.api.dto とします。

Java
package com.example.api.dto; import jakarta.validation.constraints.*; public class UserQueryParams { @NotNull(message = "page は必須項目です") @Min(value = 1, message = "page は 1 以上の整数で指定してください") private Integer page; @Min(value = 1, message = "size は 1 以上で指定してください") @Max(value = 100, message = "size は 100 以下で指定してください") private Integer size = 20; // デフォルト @Email(message = "email の形式が正しくありません") @Pattern(regexp = "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$", message = "email の形式が正しくありません") private String email; // getter / setter 省略(Lombok @Data を付与しても可) }

ポイントは次の通りです。

OpenAPI 定義 Java アノテーション コメント
required: true @NotNull 必須チェック
minimum: 1 @Min(1) 最小値
maximum: 100 @Max(100) 最大値
format: email + pattern @Email + @Pattern 形式と正規表現

ステップ 3. コントローラメソッドを DTO へ置き換える

生成されたコントローラのメソッドシグネチャを、先ほどの DTO を @ModelAttribute 経由で受け取る形に変更します。自動生成コードは変更しない ので、UserApiController の拡張クラスやカスタム実装クラスを作ります。

Java
package com.example.api.controller; import com.example.api.dto.UserQueryParams; import com.example.api.service.UserService; import jakarta.validation.Valid; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/v1") @Validated public class UserApiCustomController { private final UserService userService; public UserApiCustomController(UserService userService) { this.userService = userService; } @GetMapping("/users") public List<UserDto> getUsers(@Valid @ModelAttribute UserQueryParams params) { // バリデーションエラーがあれば例外がスローされる return userService.findUsers(params.getPage(), params.getSize(), params.getEmail()); } }
  • @Valid を付与してバリデーションを実行。
  • @ModelAttribute がクエリ文字列を DTO にバインド。
  • @Validated がクラスレベルでもアノテーションを有効化。

ステップ 4. バリデーションエラーの統一ハンドリング

Spring のデフォルト例外は JSON の構造がバラバラです。ここでは @ControllerAdvice を用いて API レスポンスを統一します。

Java
package com.example.api.exception; import org.springframework.http.*; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.*; import java.util.*; @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<Map<String, Object>> handleValidationException( MethodArgumentNotValidException ex) { List<Map<String, String>> errors = new ArrayList<>(); ex.getBindingResult().getFieldErrors().forEach(fe -> { Map<String, String> err = new HashMap<>(); err.put("field", fe.getField()); err.put("message", fe.getDefaultMessage()); errors.add(err); }); Map<String, Object> body = new LinkedHashMap<>(); body.put("timestamp", new Date()); body.put("status", HttpStatus.BAD_REQUEST.value()); body.put("error", "Bad Request"); body.put("message", "入力パラメータバリデーションエラー"); body.put("errors", errors); return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST); } // その他の例外ハンドラは必要に応じて追加 }

このハンドラにより、クエリパラメータが不正な場合は次のような JSON が返ります。

Json
{ "timestamp": "2025-08-31T12:34:56.789+09:00", "status": 400, "error": "Bad Request", "message": "入力パラメータバリデーションエラー", "errors": [ { "field": "page", "message": "page は必須項目です" }, { "field": "size", "message": "size は 100 以下で指定してください" } ] }

ステップ 5. Maven/Gradle で依存関係を整える

バリデーションに必要なライブラリを追加します(Spring Boot Starter Validation が基本)。

Xml
<!-- pom.xml --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>

Gradle の場合は:

Gradle
implementation "org.springframework.boot:spring-boot-starter-validation"

ステップ 6. 再生成時の衝突回避策

自動生成コードは target/generated-sources 配下に出力されます。手動で書いた DTO や @ControllerAdvice別パッケージ(例: com.example.api.custom)に置くことで、mvn clean./gradlew clean 後に上書きされません。さらに、openapi-generator-maven-pluginskipOverwrite オプションを有効にしておくと安全です。

Xml
<configuration> <skipOverwrite>true</skipOverwrite> </configuration>

ハマった点やエラー解決

  • @ModelAttribute が null になる
  • 原因: spring.mvc.hiddenmethod.filter.enabled がデフォルトで false になっていた。application.ymlspring.mvc.hiddenmethod.filter.enabled: true を追記すると解決。
  • @Valid が効かずエラーがスローされない
  • 原因: コントローラに @Validated が付いていなかった。クラスレベルに @Validated を忘れずに付与。
  • MethodArgumentNotValidException がハンドラに捕まらない
  • 原因: @RestControllerAdvice が別パッケージに配置され、コンポーネントスキャン対象外だった。@ComponentScan でパッケージを拡張するか、同一ルートパッケージに置く。

解決策まとめ

問題 原因 解決策
DTO がバインドされない @ModelAttribute が正しく機能していない spring.mvc.hiddenmethod.filter.enabled: true を設定
バリデーションが走らない @Validated が抜けている コントローラに @Validated を付与
統一エラーレスポンスが出ない @RestControllerAdvice がスキャン対象外 パッケージ構成を見直すか @ComponentScan を調整

まとめ

本記事では、OpenAPI で自動生成した Java API に対し、クエリパラメータのバリデーションを安全かつ再利用可能な形で実装する手順を解説しました。

  • OpenAPI 定義にバリデーション情報を記述し、minimum / maximum / pattern などを活用
  • DTO に jakarta.validation アノテーションを付与し、@ModelAttribute@Valid で自動バインド・検証
  • @ControllerAdvice による統一エラーハンドリングで、クライアントに分かりやすい JSON を返却
  • 再生成時の衝突回避としてコード分離と skipOverwrite 設定を実施

これにより、フロントエンドとサーバ側のバリデーションが整合し、不正リクエストの早期検出と統一エラーレスポンスが実現できます。次のステップとして、リクエストボディ(POST/PUT)へのバリデーション拡張や、OpenAPI の components.schemas から自動的に DTO を生成するカスタムプラグインの作成に挑戦したいと考えています。

参考資料