バリデーション設計は入力チェックの置き場所で差が出る
バリデーション設計で迷う場面は、Spring Boot実務ではかなり多いです。DTOにアノテーションを書くべきか。Controllerでチェックするべきか。Service層で業務ルールを見るべきか。こうした判断が曖昧なままだと、入力チェックが散らばります。
結論から言うと、Spring Bootのバリデーションは形式チェック、相関チェック、業務ルール、状態チェックを分けて設計します。すべてをBean Validationに寄せる必要はありません。一方で、すべてをService層に丸投げする設計も保守しにくくなります。
この記事では、Java/Spring Bootの実務でレビューされやすい観点を整理します。Request DTO、@Valid、Controller、Service層、業務ルール、エラーレスポンスの分担を扱います。
Spring Bootのバリデーション設計で分けたい4種類
まず、入力チェックを1つの言葉でまとめないことが大切です。実務では、同じ「バリデーション」でも責務が違います。
| 種類 | 例 | 置き場所の目安 |
|---|---|---|
| 形式チェック | 必須、桁数、メール形式、日付形式 | Request DTO |
| 相関チェック | 開始日が終了日以前、どちらか一方は必須 | DTOのクラスレベル制約、専用Validator |
| 業務ルール | 同じメールアドレスは登録不可、在庫不足 | Service層、Domain |
| 状態チェック | 承認済み注文は変更不可、退会済みユーザーは操作不可 | Service層、Domain |
形式チェックは、APIの入力契約に近いです。そのため、Request DTOに置くと読みやすくなります。一方で、DBや外部APIを見ないと判断できないものは、DTOだけでは決められません。
Spring Boot公式のValidationでは、Bean Validation APIを利用できることが説明されています。標準的な仕組みを使いながら、どこまでをアノテーションで表すかを設計します。
Request DTOには形式チェックを置く
Request DTOには、リクエスト単体で判断できるチェックを置きます。例えば、必須、最大長、最小値、メール形式などです。
public record CreateUserRequest(
@NotBlank
@Email
String email,
@NotBlank
@Size(min = 8, max = 72)
String password,
@NotBlank
@Size(max = 50)
String displayName
) {}
この形にすると、ControllerやService層に基本的な入力チェックを書かずに済みます。Request DTOを見れば、APIが受け付ける入力の前提もわかります。
ただし、DTOのアノテーションを増やしすぎると読みづらくなります。複雑な条件を無理に1つのDTOへ詰め込むより、用途別のRequest DTOに分ける方が自然です。
Controllerはバリデーションを起動する入口にする
Controllerでは、入力チェックの詳細を書くより、Request DTOを受け取り、バリデーションを起動する入口にします。Spring MVCでは、Controllerメソッドの引数に対する検証を扱えます。公式のSpring MVC Validationにも関連する説明があります。
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
@PostMapping
public ResponseEntity<UserResponse> create(
@Valid @RequestBody CreateUserRequest request
) {
UserResponse response = userService.create(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
}
Controllerにif文が増えると、HTTP、入力検証、業務処理の責務が混ざります。最初は小さなチェックでも、APIが増えると重複しやすくなります。
そのため、Controllerは薄く保ちます。入力形式はDTOへ寄せます。業務判断はService層へ渡します。この分担がレビューでも見られやすいポイントです。
業務ルールはService層やDomainで検証する
次に、業務ルールはRequest DTOだけでは判断できません。例えば、メールアドレスの重複、在庫数、権限、ステータス遷移は、DBの状態やログインユーザーの情報が必要です。
@Service
public class UserService {
@Transactional
public UserResponse create(CreateUserRequest request) {
if (userRepository.existsByEmail(request.email())) {
throw new BusinessException("USER_EMAIL_ALREADY_EXISTS");
}
User user = User.create(
request.email(),
passwordEncoder.encode(request.password()),
request.displayName()
);
return UserResponse.from(userRepository.save(user));
}
}
この例では、メール形式はDTOで確認します。メールアドレスが既に使われているかはService層で確認します。理由は、Repositoryを使った状態確認が必要だからです。
なお、Domainモデルを厚くする設計では、状態遷移や不変条件をDomain側へ寄せることもあります。重要なのは、業務ルールをControllerやDTOに散らばらせないことです。
相関チェックは無理にフィールド単位にしない
開始日と終了日、配送先と請求先、パスワードと確認用パスワードなどは、1項目だけでは判断できません。こうした相関チェックをフィールド単位のアノテーションで無理に表すと、意図が読みにくくなります。
選択肢は大きく2つです。DTOのクラスレベル制約として表す方法があります。もう1つは、Service層に入る前の専用Validatorで明示的に確認する方法です。
Spring Framework公式のJava Bean Validationでは、Bean ValidationをSpringで扱う仕組みが説明されています。実務では、標準の制約で足りるものと、独自制約にするものを分けて考えると判断しやすくなります。
バリデーションエラーのレスポンス設計も合わせて考える
バリデーション設計では、チェックの場所だけでなく、エラーの返し方も重要です。画面がフォームなら、field単位で返せる形式が扱いやすくなります。
{
"code": "VALIDATION_ERROR",
"message": "入力内容を確認してください。",
"errors": [
{
"field": "email",
"code": "Email",
"message": "メールアドレスの形式で入力してください。"
}
]
}
エラーレスポンスがAPIごとに違うと、フロントエンド側の実装が複雑になります。ReactやTypeScriptと連携する案件では、バリデーションエラーの型も重要です。
また、表示用メッセージと内部エラーコードは分けると扱いやすいです。ログ調査、画面表示、多言語対応、問い合わせ対応で使う情報が違うためです。
実務レビューで見られやすいアンチパターン
レビューでは、動くかどうかだけでなく、変更しやすいかも見られます。特に、入力チェックが複数の層に重複している場合は注意が必要です。
| 指摘されやすい例 | 起きる問題 | 見直すポイント |
|---|---|---|
| Controllerにif文で入力チェックを書く | APIごとに重複しやすい | 形式チェックはRequest DTOへ寄せる |
| DTOに業務ルールを詰め込む | Repositoryや認可情報が必要になり責務が崩れる | Service層やDomainへ移す |
| 同じチェックをDTOとServiceで二重に書く | 仕様変更時に片方だけ漏れる | チェックの種類で置き場所を決める |
| エラーメッセージを直書きする | 変更、翻訳、テストがしにくい | エラーコードとメッセージ管理を分ける |
| バリデーション例外と業務例外を混ぜる | HTTPレスポンスが不安定になる | 例外種別とレスポンス変換を整理する |
特に、バリデーション例外と業務例外の扱いはAPI設計にも関わります。入力形式のエラーなのか、業務上許可できない操作なのかを分けると、HTTPステータスやエラーコードを決めやすくなります。
テストでは境界値と業務ルールを分けて確認する
バリデーション設計は、テスト設計ともつながります。DTOの形式チェックは、Controller層のテストやValidatorのテストで確認できます。一方で、重複チェックや状態遷移はService層のテストで確認します。
- 必須項目が未入力のときにfield単位のエラーになるか
- 文字数の境界値で期待どおりに通るか
- メール形式など標準制約が有効になっているか
- 重複データがあると業務例外になるか
- 状態によって許可されない操作を止められるか
ここを分けておくと、テストの意図が読みやすくなります。失敗したときも、DTOの制約が悪いのか、業務判断が悪いのかを切り分けやすくなります。
よくある質問
バリデーションはすべてDTOに書くべきですか?
すべてDTOに書く必要はありません。必須、桁数、形式のようにリクエスト単体で判断できるものはDTOに向いています。一方で、DB状態、権限、ステータス遷移が必要な判断はService層やDomainに置く方が自然です。
Controllerで入力チェックを書いてはいけませんか?
絶対に書いてはいけないわけではありません。ただし、Controllerに入力チェックが増えると、HTTP処理と業務判断が混ざります。まずはRequest DTOとBean Validationで表せないかを検討します。
業務エラーは400で返すべきですか?
プロジェクトのAPI設計方針によります。入力形式のエラー、権限エラー、リソース状態によるエラーは意味が違います。HTTPステータス、エラーコード、表示メッセージのルールを先に決めることが重要です。
まとめ
Spring Bootのバリデーション設計では、入力チェックを1か所に押し込めないことが大切です。形式チェックはRequest DTOに置きます。相関チェックはクラスレベル制約や専用Validatorを検討します。業務ルールや状態チェックはService層やDomainで扱います。
また、エラーレスポンスの設計も合わせて考えます。field、code、messageを整理しておくと、フロントエンドとの連携や障害調査が進めやすくなります。
Java/Spring Bootの実務では、バリデーション設計、DTO設計、API設計、例外処理、テスト設計がつながります。bluenaの採用情報では、Spring Boot経験を活かせる案件相談も行っています。今の経験を次の環境でどう伸ばすか、一度整理してみてください。
一度カジュアル面談をしませんか?
株式会社bluenaは「高還元」と「伴走支援」を両立したSES企業です。単価の81〜86%を還元する報酬体系と、専任サポーターによる隔週1on1で、エンジニアが納得できるキャリアを実現します。
まとまっていなくてもOK——まずは現在地を聞かせてください。
カジュアル面談ですので、お気軽にお聞かせください。





