markdown
はじめに (対象読者・この記事でわかること)
本記事は、Java(特にSpring Boot)でWebサービスを開発しているエンジニア、もしくはOpenAPI(Swagger)からコードを自動生成した経験がある方を対象としています。
OpenAPI の openapi.yaml から springdoc-openapi などのジェネレータでコントローラ・DTO が自動生成されたプロジェクトに対し、クエリパラメータの入力制約(必須・形式・範囲)をサーバ側で確実にチェックしたいというニーズに応える内容です。
この記事を読むことで、以下が実現できるようになります。
- OpenAPI の
parameter定義を Java のバリデーションアノテーションへ変換する手順 - Spring の
@Validatedと@Valid、BindingResultを使った実装パターン - バリデーションエラー時のカスタム例外ハンドラと統一レスポンスの作り方
背景として、フロントエンド側でのバリデーションは必須ですが、サーバ側での二重チェックが欠如すると不正データがデータベースに流入したり、予期しない例外が発生したりします。自動生成コードに手を加える際のベストプラクティスを示すことで、保守性と安全性を同時に高められます。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- Java 11 以上、Spring Boot 2.7 以降の基本的な使い方
- Maven または Gradle のビルド設定、依存関係の追加方法
- OpenAPI(Swagger)仕様書の基本構造(paths、parameters、schemas)
OpenAPI で生成されたコードの概要とバリデーションが必要になる背景
springdoc-openapi-maven-plugin などを用いると、openapi.yaml の parameters 定義から次のようなコントローラメソッドが自動生成されます。
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) { // 省略 }
このままでは、以下の問題が残ります。
- 必須項目のチェックが自動で入らない
OpenAPI ではrequired: trueと記述していても、生成コードはrequired = falseになることがある。 - 形式・範囲の制約が失われる
minimum,maximum,patternなどのバリデーション情報は、Java のバリデーションアノテーション (@Min,@Max,@Pattern) に変換されていない。 - エラーメッセージが統一されていない
Spring の標準例外 (MethodArgumentTypeMismatchExceptionなど) がそのまま返却され、API 利用者にとっては意味不明な JSON が返る。
そこで、OpenAPI のパラメータ定義と Java バリデーションを橋渡しする仕組みを自前で用意します。主に以下の三点に注目します。
- DTO に変換してバリデーションアノテーションを付与
コントローラの引数を@ModelAttributeで束ねたクラスに置き換え、@NotNull,@Size,@Patternなどを付与。 @ValidatedとBindingResultでバリデーション結果を取得
エラーがあればカスタム例外にラップし、統一フォーマットでレスポンス。- コード自動生成と手動カスタマイズの分離
生成されたコードはそのまま残し、@Generatedアノテーションでマークした上に手動で追加した DTO を別ファイルに配置。再生成時に上書きされないようにする。
以下の章で、実際に手を動かしながらこのフローを構築していきます。
クエリパラメータバリデーション実装のステップバイステップ
ステップ 1. OpenAPI 定義にバリデーション情報を明示する
まずは openapi.yaml にバリデーション情報を記述します。例としてユーザー一覧取得 API のクエリパラメータを拡張します。
Yamlpaths: /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: emailとpattern→ 正規表現による形式チェック
この情報はコード生成時に利用できるようにしておくと、後述の 自動マッピング が容易になります。
ステップ 2. 生成コードにバリデーション用 DTO を追加
自動生成されたコントローラはそのまま残し、クエリパラメータ用の DTO を手動で作成します。パッケージは com.example.api.dto とします。
Javapackage 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 の拡張クラスやカスタム実装クラスを作ります。
Javapackage 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 レスポンスを統一します。
Javapackage 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 の場合は:
Gradleimplementation "org.springframework.boot:spring-boot-starter-validation"
ステップ 6. 再生成時の衝突回避策
自動生成コードは target/generated-sources 配下に出力されます。手動で書いた DTO や @ControllerAdvice は 別パッケージ(例: com.example.api.custom)に置くことで、mvn clean や ./gradlew clean 後に上書きされません。さらに、openapi-generator-maven-plugin の skipOverwrite オプションを有効にしておくと安全です。
Xml<configuration> <skipOverwrite>true</skipOverwrite> </configuration>
ハマった点やエラー解決
@ModelAttributeが null になる- 原因:
spring.mvc.hiddenmethod.filter.enabledがデフォルトでfalseになっていた。application.ymlにspring.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 を生成するカスタムプラグインの作成に挑戦したいと考えています。
参考資料
- Spring Boot Reference – Validation
- OpenAPI Specification v3.1.0 – Parameter Object
- jakarta.validation – Bean Validation 3.0
- openapi-generator-maven-plugin – Documentation
