スポンサーリンク

Spring Bootの入力チェック完全ガイド:@Validとエラー返却まで

スポンサーリンク
この記事は約34分で読めます。
スポンサーリンク

「@Valid付けたのに、なんで普通に通っちゃうんですか…?」とか、「急に400返ってきて何が悪いのか分からないんですが…」みたいなやつ、Spring Bootでバリデーション(入力チェック)を触るとめちゃくちゃ出ますよね。
あと地味に多いのが、入れ子DTOだけスルーされるとか、Listの中身だけ素通りするパターンです。こっちは見た目ちゃんと書けてるのに、動かないと本当にモヤっとします。

この記事では、そのモヤモヤをちゃんと終わらせます。
やることはシンプルで、まずはコピペで動く最小例で「これが正しい形」を体験してもらいます。

次に、動かないときのための原因チェックリストで「どこが抜けがちか」を一気に潰します。
そのうえで、入れ子DTO・配列・Listの「素通り」を止めるコツ、そして実務で一番困るエラーの返し方(画面/WebとAPI/JSON)まで、ちゃんとテンプレでまとめます。

最後に、@Validatedの使いどころ(場面切替)や、メッセージの日本語化もやります。

読み終わる頃には、「@Validが動かない」「入れ子が効かない」「エラーを綺麗に返せない」が全部セットで解決して、自分のプロジェクトにそのまま持ち込める“型”ができている状態にしますね。

  1. まずはコピペでOK!最小の動くバリデーション
    1. 依存関係:starter-validation
    2. DTOに付ける:よく使う基本アノテーション
    3. Controllerに付ける:@Validが「スイッチ」
    4. 動いたか確認:OK/NGのリクエスト例
  2. 「@Validが動かない」原因チェックリスト(ここを見れば直る)
    1. 依存関係が入ってない/importが違う
    2. 付ける場所が違う(Controller引数/入れ子/List)
    3. 例外が握りつぶされている/ログが見えていない
    4. まとめ:最速で直すなら、ここだけ見てください
  3. 入れ子DTO・配列・Listのチェックを通すコツ
    1. 入れ子DTO:親のフィールドに@Validを付ける
    2. Listの中身:List<@Valid ItemDto>の形にする
    3. 「要素数」も「要素の中身」も両方チェックする例
    4. どこに付けるか迷った時の「図っぽい覚え方」
  4. エラーの返し方2パターン:画面(Web)とAPI(JSON)
    1. 画面向け:BindingResultで受け取って同じ画面へ
    2. API向け:例外を拾ってエラー一覧JSONに整形
    3. 400になる理由:自動で弾かれる仕組みを理解する
  5. @Validと@Validatedの違いを一言で+場面で使い分け
    1. 一言まとめ:@Validは基本、@Validatedは「場面切替」
    2. 作成だけ必須/更新は任意:グループバリデーション
    3. Serviceでもチェック:クラスに@Validatedを付ける
  6. よく使うアノテーション早見表(迷ったらここ)
    1. 文字:NotNull/NotBlank/NotEmptyの違い
    2. 長さ・数:Size/Min/Max(ざっくり選び方)
    3. 形式:Email/Pattern(やりすぎ注意ポイント)
    4. 迷ったらこの選び方でOKです
  7. エラーメッセージを日本語にする・分かりやすくする
    1. message=””で手早く変更(まずはここ)
    2. messages.propertiesで一括管理(日本語化に強い)
    3. JSONの返し方テンプレ:field別に配列で返す
  8. つまずきQ&A(検索されがちな疑問を一気に解決)
    1. BindingResultって何?付けないと例外?
    2. 入れ子がチェックされないのはなぜ?
    3. ControllerとService、どっちでやるのが正解?
  9. まとめ:今日から困らないための「型」を作ろう
    1. この記事のチェックリスト再掲
    2. 次にやると良いこと(テスト/共通エラー形式)
スポンサーリンク

まずはコピペでOK!最小の動くバリデーション

ここは難しい話いったんナシです。
「依存関係を入れる → DTOにルールを書く → Controllerで@Validを付ける」これだけで動きます。

まずはコピペして、動いた感を掴んでくださいです!

依存関係:starter-validation

Spring Bootで入力チェックを動かすには、まずこれが必要です。
これが入ってないと、@Validを付けても基本的に何も起きません(あるある最上位です…)。

Maven(pom.xml)

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

Gradle(build.gradle)

dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-validation'
}

裏側ではだいたい Hibernate Validator(無料) が動いてくれて、アノテーションのルールをチェックしてくれる感じです。

DTOに付ける:よく使う基本アノテーション

最小例として「ユーザー登録っぽいDTO」を作ります。
この3つ(name/email/password)で、だいたいの現場の入口はカバーできます。

package com.example.demo.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public class SignupRequest {

  @NotBlank(message = "名前は必須です")
  @Size(max = 20, message = "名前は20文字以内にしてください")
  private String name;

  @NotBlank(message = "メールは必須です")
  @Email(message = "メールの形式が変です")
  private String email;

  @NotBlank(message = "パスワードは必須です")
  @Size(min = 8, message = "パスワードは8文字以上にしてください")
  private String password;

  // getter/setter(またはrecordでもOK)
  public String getName() { return name; }
  public void setName(String name) { this.name = name; }
  public String getEmail() { return email; }
  public void setEmail(String email) { this.email = email; }
  public String getPassword() { return password; }
  public void setPassword(String password) { this.password = password; }
}

ポイントはこれだけです。

  • @NotBlank:空文字やスペースだけもNGにしたいとき
  • @Size:文字数の上限/下限
  • @Email:メールっぽい形式かどうか(厳密すぎる時もあるので、使いすぎ注意は後で触れます)

Controllerに付ける:@Validが「スイッチ」

DTOにルールを書いただけでは、まだチェックが走らないことがあります。
Controllerの引数で @Valid を付けて初めて「チェックして!」の合図になります。

package com.example.demo.controller;

import com.example.demo.dto.SignupRequest;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class SignupController {

  @PostMapping("/signup")
  public ResponseEntity<String> signup(@RequestBody @Valid SignupRequest request) {
    // ここに来た時点で、バリデーションOKのデータです
    return ResponseEntity.ok("ok");
  }
}

ここで大事なのは配置です。

  • @RequestBody @Valid SignupRequest request ← この形が基本
  • ❌ DTOにだけ書いて、Controllerで@Valid付けない ← ありがち

動いたか確認:OK/NGのリクエスト例

動作確認は curl で十分です(PostmanでももちろんOKです)。

OK例(通る)

curl -X POST http://localhost:8080/api/signup \
  -H "Content-Type: application/json" \
  -d '{"name":"たろう","email":"taro@example.com","password":"password123"}'

期待:ok が返る(200)

NG例(落ちる)

curl -X POST http://localhost:8080/api/signup \
  -H "Content-Type: application/json" \
  -d '{"name":"","email":"xxx","password":"123"}'

期待:だいたい 400 Bad Request になります。
この段階だとエラーの見え方は環境によって少し違いますが、「nameが必須」「email形式が違う」「password短い」みたいな内容が返る(or ログに出る)はずです。

「え、なんで勝手に400になるんです?」は超あるあるなので、H2-4で“仕組み”からスッキリ説明しますね。
そして「JSONを綺麗に整形して返すテンプレ」もそこで出します。

Maven Repository: org.springframework.boot » spring-boot-starter-validation
Maven Repository: org.hibernate » hibernate-validator

「@Validが動かない」原因チェックリスト(ここを見れば直る)

「ちゃんと@Valid付けたのに、普通に通るんですが…?」ってときは、だいたい原因は決まってます。
ここはYES/NOで潰せるチェックリストにするので、上から順に見ていけば直るはずです!

依存関係が入ってない/importが違う

Q1. spring-boot-starter-validation 入ってます?

  • ✅ YES → 次へ
  • ❌ NO → 入れた瞬間に直ることが多いです(H2-1の1行追加)

Q2. import が jakarta.validation.Valid になってます?

  • ✅ YES → 次へ
  • ❌ NO → ここめっちゃ罠です

よくあるNG例:

import javax.validation.Valid; // ←古い(環境によっては動かない/混ざると混乱)

基本はこっちです:

import jakarta.validation.Valid;

IntelliJやVS Codeで自動import任せにすると、たまに違う方が入ることがあるので、ここは一回だけ目視チェックおすすめです!

付ける場所が違う(Controller引数/入れ子/List)

Q3. Controllerの引数に@Valid付けてます?(DTOに付けただけで満足してない?)

  • ✅ YES → 次へ
  • ❌ NO → **Controllerの引数が“スイッチ”**です

OK例(これが基本):

public ResponseEntity<?> signup(@RequestBody @Valid SignupRequest request) { ... }

ありがちNG例(DTOに注釈はあるけど、チェック開始してない):

public ResponseEntity<?> signup(@RequestBody SignupRequest request) { ... } // @Validなし

Q4. 入れ子DTOがあるなら「親のフィールド」に@Valid付けてます?

  • ✅ YES → 次へ
  • ❌ NO → 入れ子だけ素通りします

例(NG):

public class ParentDto {
  private ChildDto child; // ← ここに@Validがないと、ChildDtoの中はチェックされません
}

例(OK):

public class ParentDto {
  @jakarta.validation.Valid
  private ChildDto child;
}

Q5. Listの中身をチェックしたいなら「要素側」に@Valid付けてます?

  • ✅ YES → 次へ
  • ❌ NO → Listの外側だけ見て中身スルーが起きます

よくある勘違い(外側だけ):

private List<ItemDto> items; // ← 中身のItemDtoの注釈が実行されないことがある

定番の書き方(要素を@Valid):

private List<@jakarta.validation.Valid ItemDto> items;

※ 入れ子/Listの話は 次で「最小コード+組み合わせ例」をガッツリやります!


例外が握りつぶされている/ログが見えていない

Q6. バリデーションエラーが出てるのに、例外ハンドリングで“無かったこと”にしてません?

  • ✅ YES → それが原因の可能性大
  • ❌ NO → 次へ

ありがちなパターンはこれです:

  • @ExceptionHandler(Exception.class) みたいなので雑に拾って、常に200返してる
  • ログ出してないから「何が起きたか」見えない
  • 逆に、独自のフィルタ/インターセプタで例外を別の形に潰してる

まずは最低限、これだけ意識すると速いです。

  • Controllerに来る入力チェックは、失敗すると だいたい400系の例外になります
  • それを @RestControllerAdvice で拾って、ちゃんとログとJSONにするのが王道

「JSONをどう整形するのが正解?」は この後にテンプレを用意しています。
ここで悩むの、普通です。悩まなくてOKです!

まとめ:最速で直すなら、ここだけ見てください

最後に超まとめです。動かない時はほぼこれ。

  • starter-validation入ってる?
  • importはjakarta?
  • Controller引数に@Valid付いてる?
  • 入れ子の親フィールドに@Valid付いてる?
  • Listは要素側に@Valid付いてる?
  • 例外がどこかで握りつぶされてない?
IntelliJ IDEA のダウンロード
業界をリードするプロ仕様の Java および Kotlin 開発向け IDE である IntelliJ IDEA の最新バージョンをダウンロードしましょう。 Windows、macOS、および Linux に対応しています。
https://azure.microsoft.com/ja-jp/products/visual-studio-code
Postman: The World's Leading API Platform | Sign Up for Free
Accelerate API development with Postman's all-in-one platform. Streamline collaboration and simplify the API lifecycle f...
筆者
筆者

次は、入れ子DTO・配列・Listの「素通り」を完全に止める書き方を、結論→最小コードでいきますね。

入れ子DTO・配列・Listのチェックを通すコツ

ここ、結論だけ先に言いますね。この2つが“素通り防止のルール”です。

  • 入れ子DTO:親のフィールドに @Valid を付ける(入れ子ONのスイッチ)
  • Listの中身:List<@Valid ItemDto> の形にする(要素の中身まで見るスイッチ)

これがないと、子どものDTOがどれだけルール書いてても「見ないでスルー」されがちです…!

入れ子DTO:親のフィールドに@Validを付ける

まず入れ子の最小例です。
「住所(Address)がユーザー(User)の中に入ってる」みたいな形ですね。

子(入れ子側)

package com.example.demo.dto;

import jakarta.validation.constraints.NotBlank;

public class AddressDto {

  @NotBlank(message = "郵便番号は必須です")
  private String zip;

  @NotBlank(message = "住所は必須です")
  private String line1;

  public String getZip() { return zip; }
  public void setZip(String zip) { this.zip = zip; }
  public String getLine1() { return line1; }
  public void setLine1(String line1) { this.line1 = line1; }
}

親(ここが重要)

package com.example.demo.dto;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;

public class UserRequest {

  @NotBlank(message = "名前は必須です")
  private String name;

  @Valid // ← これがないと AddressDto の中の @NotBlank が動かないことが多いです
  private AddressDto address;

  public String getName() { return name; }
  public void setName(String name) { this.name = name; }
  public AddressDto getAddress() { return address; }
  public void setAddress(AddressDto address) { this.address = address; }
}

ポイント@Valid は「その中も見てチェックしてね」の合図です。
親に付けないと、子の注釈は読まれない=入れ子だけ素通り、になりやすいです。

Listの中身:List<@Valid ItemDto>の形にする

次は「注文(Order)に明細(items)がある」みたいなやつです。
ここでの落とし穴は、List自体は見ても、中身のDtoを見ないパターンです。

要素(Item)

package com.example.demo.dto;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;

public class ItemDto {

  @NotBlank(message = "商品名は必須です")
  private String itemName;

  @Min(value = 1, message = "数量は1以上にしてください")
  private int quantity;

  public String getItemName() { return itemName; }
  public void setItemName(String itemName) { this.itemName = itemName; }
  public int getQuantity() { return quantity; }
  public void setQuantity(int quantity) { this.quantity = quantity; }
}

親(Order)— Listの要素側に@Valid

package com.example.demo.dto;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import java.util.List;

public class OrderRequest {

  @NotEmpty(message = "明細は1件以上必要です") // ← 「要素数」のチェック(外側)
  private List<@Valid ItemDto> items;        // ← 「中身」のチェック(内側)

  public List<ItemDto> getItems() { return items; }
  public void setItems(List<ItemDto> items) { this.items = items; }
}

ポイントは二段構えです。

  • @NotEmpty:Listが空じゃないこと(外側)
  • List<@Valid ItemDto>:各ItemDtoの中もチェック(内側)

「要素数」も「要素の中身」も両方チェックする例

じゃあ実際に、どんなリクエストが落ちるの?をまとめて見せますね。

NG①:Listが空(外側ルール違反)

{
  "items": []
}
  • 明細は1件以上必要です

NG②:Listはあるけど中身がダメ(内側ルール違反)

{
  "items": [
    { "itemName": "", "quantity": 0 }
  ]
}
  • 商品名は必須です
  • 数量は1以上にしてください

OK:外側も内側もOK

{
  "items": [
    { "itemName": "えんぴつ", "quantity": 2 }
  ]
}

どこに付けるか迷った時の「図っぽい覚え方」

コード見るより、場所だけ覚えるならこれです。

  • Controller引数@Valid(チェック開始スイッチ)
  • 入れ子(親→子):親のフィールドに @Valid
  • List(外側→要素)List<@Valid 要素Dto>(+外側に@NotEmptyなど)

つまり、「深いところを見たい場所に@Valid」って覚えるとラクです!

筆者
筆者

次は、バリデーションで落ちたときに「どう返すのが正解?」問題を片付けますね。
画面用(BindingResult)とAPI用(JSON整形)で、テンプレ2本立ていきます。

エラーの返し方2パターン:画面(Web)とAPI(JSON)

バリデーションが動いた次に絶対つまずくのがここです。
「落ちたのは分かったけど、どう返すのが正解なんです?」問題ですね。

結論、よく使うのはこの2つです。

  • 画面(Web)向けBindingResult で受け取って、同じ画面に戻す
  • API(JSON)向け:例外を @RestControllerAdvice で拾って、エラー一覧JSONに整形する

画面向け:BindingResultで受け取って同じ画面へ

画面(HTML)なら、基本は「入力画面に戻してエラー表示」です。
そのための受け皿が BindingResult です。

✅ 最小例(フォーム送信 → エラーなら同じ画面へ)

package com.example.demo.web;

import jakarta.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

@Controller
@RequestMapping("/signup")
public class SignupPageController {

  @GetMapping
  public String showForm(SignupForm form) {
    return "signup";
  }

  @PostMapping
  public String submit(@ModelAttribute @Valid SignupForm form,
                       BindingResult bindingResult, // ← ここが受け皿
                       Model model) {

    if (bindingResult.hasErrors()) {
      // エラーがあるなら同じ画面に戻す(テンプレ側で表示)
      return "signup";
    }

    // 登録処理など
    return "redirect:/signup/complete";
  }
}

超重要ポイントBindingResult@Validを付けた引数の“直後”に置くのが定番です。
これがあると「例外で落とす」じゃなくて「エラーとして受け取る」動きになります。

API向け:例外を拾ってエラー一覧JSONに整形

API(JSON)だと、同じ画面に戻せないので、フロントが扱いやすいエラーJSONを返すのが王道です。
Spring Bootはそのままだと400で落としてくれるんですが、返る形がバラつくので、Adviceで統一します。

✅ テンプレ(field別に配列で返す+globalも返す)

package com.example.demo.api;

import org.springframework.http.*;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;

import java.time.OffsetDateTime;
import java.util.*;

@RestControllerAdvice
public class ApiExceptionHandler {

  @ExceptionHandler(MethodArgumentNotValidException.class)
  public ResponseEntity<Map<String, Object>> handle(MethodArgumentNotValidException ex) {

    Map<String, List<String>> fieldErrors = new LinkedHashMap<>();
    List<String> globalErrors = new ArrayList<>();

    ex.getBindingResult().getFieldErrors().forEach(err -> {
      fieldErrors.computeIfAbsent(err.getField(), k -> new ArrayList<>())
                 .add(err.getDefaultMessage());
    });

    ex.getBindingResult().getGlobalErrors().forEach(err -> {
      globalErrors.add(err.getDefaultMessage());
    });

    Map<String, Object> body = new LinkedHashMap<>();
    body.put("message", "入力内容にエラーがあります");
    body.put("errors", fieldErrors);          // ← {field:[msg...]}
    body.put("global", globalErrors);         // ← 全体エラーがある場合
    body.put("timestamp", OffsetDateTime.now().toString());

    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body);
  }
}

返るイメージはこんな感じです(例):

{
  "message": "入力内容にエラーがあります",
  "errors": {
    "name": ["名前は必須です"],
    "email": ["メールの形式が変です"],
    "password": ["パスワードは8文字以上にしてください"]
  },
  "global": [],
  "timestamp": "2025-12-21T10:12:34+09:00"
}

400になる理由:自動で弾かれる仕組みを理解する

「なんで勝手に400になるんです?」の答えはこれです。

  • @RequestBody @Valid の引数は、Controllerメソッドに入る前にSpringがチェックします
  • NGなら、Springが 例外(MethodArgumentNotValidException など)を投げます
  • その例外をSpringが「これは不正なリクエストだね」と判断して、400を返す流れです

なので、

  • BindingResultを置く(画面向け) → 例外にせず、エラーとして受け取れる
  • Adviceで拾う(API向け) → 例外は出るけど、返すJSONを統一できる

っていう使い分けになります。

筆者
筆者

次は、@Valid@Validated の違いを「暗記」じゃなく「使い分け」で理解できるようにしますね。
作成だけ必須/更新は任意、みたいな 場面切替(グループ)を最小例でいきます。

@Validと@Validatedの違いを一言で+場面で使い分け

一言まとめ:@Validは基本、@Validatedは「場面切替」

  • @Valid:いつもの入力チェック(デフォルトのルールでチェック)
  • @Validated「作成のときだけ厳しく」「更新のときは別ルール」みたいに、場面(グループ)を切り替えてチェックしたいときに使うやつです

普段は@ValidでOKで、「ルール切替が必要になったら@Validated」って覚え方が一番ラクです。

次は、@Valid@Validated の違いを「暗記」じゃなく「使い分け」で理解できるようにしますね。
作成だけ必須/更新は任意、みたいな 場面切替(グループ)を最小例でいきます。

作成だけ必須/更新は任意:グループバリデーション

まず“グループ名”を作ります(ただの目印です)。

public interface Create {}
public interface Update {}

DTO側で「どの場面で効かせるか」を指定します。

import jakarta.validation.constraints.*;

public class UserDto {

  @NotNull(groups = Update.class, message = "更新ではidが必須です")
  private Long id;

  @NotBlank(groups = {Create.class, Update.class}, message = "名前は必須です")
  private String name;

  @NotBlank(groups = Create.class, message = "作成ではパスワード必須です")
  @Size(min = 8, groups = Create.class, message = "作成では8文字以上にしてください")
  private String password;
}

Controllerで「今回はどの場面?」を選びます。

import org.springframework.validation.annotation.Validated;

@PostMapping("/users")
public void create(@RequestBody @Validated(Create.class) UserDto dto) {}

@PutMapping("/users/{id}")
public void update(@RequestBody @Validated(Update.class) UserDto dto) {}

これで「作成はpassword必須、更新はpassword任意」みたいな切替ができます。

Serviceでもチェック:クラスに@Validatedを付ける

Controllerだけじゃなく、Serviceの入口でも守ると事故が減ります(Controller以外から呼ばれる道が増えがちなので…)。

import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import jakarta.validation.Valid;

@Service
@Validated
public class UserService {

  public void register(@Valid UserDto dto) {
    // ここに来る前にチェックが走ります(外部から呼ばれた場合)
  }
}

※Serviceのチェックは「Springの管理するBean越しに呼ばれる」ときに効くイメージでOKです(同じクラス内で自分自身を呼ぶ形だと効かないことがあります)。

よく使うアノテーション早見表(迷ったらここ)

「NotNullとNotBlank、どっちだっけ…」って毎回なりますよね。大丈夫です。
ここは“いつ使う?”でサクッと選べるようにまとめます。迷ったらこの章に戻ってくればOKです!

文字:NotNull/NotBlank/NotEmptyの違い

まず結論だけ言うと、こんな感じです。

  • NotNullnull じゃなければOK(空文字 "" はOK)
  • NotEmpty:空っぽはNG(文字なら "" NG、Listなら [] NG)
  • NotBlank:空っぽもスペースだけもNG(文字列に一番よく使う)
アノテーション主に使う対象NGになる例OKになる例ありがちなミス
@NotNullなんでもnull"" / [] / 0「必須」のつもりで文字列に使って、空文字が通る
@NotEmpty文字・List・配列"" / []"a" / [1]文字列で「スペースだけ」は通ることがある
@NotBlank文字列専用null / "" / " ""a"数値やListには使えない

おすすめの覚え方

  • 文字の必須:だいたい NotBlank
  • Listの必須(1件以上):だいたい NotEmpty
  • 「nullだけ防げればいい」:NotNull

長さ・数:Size/Min/Max(ざっくり選び方)

ここは「文字数」と「数値」で使い分けです。

  • Size:長さ(文字数・List件数)
  • Min/Max:数値の最小・最大
アノテーション使う対象よくある使い方
@Size(min=, max=)文字列 / List / 配列@Size(max=20)名前は20文字まで、明細は1〜50件まで
@Min(1)数値@Min(1)数量は1以上、年齢は0以上など
@Max(100)数値@Max(100)上限がある数字(点数、割引率など)

ミスりがちポイント

  • 「文字列の長さ」を @Max でやろうとしてしまう → 文字はSizeです
  • 「Listの件数チェック」を @Min でやろうとしてしまう → ListはSizeです

形式:Email/Pattern(やりすぎ注意ポイント)

形式チェックは便利なんですが、やりすぎると利用者が詰みます。ほどほどが正解です。

  • Email:メールっぽい形式
  • Pattern:正規表現で自由にルール(強いけど危険)
アノテーション使いどころ注意
@Emailメール入力厳密すぎると弾きすぎることもあるので、必須は@NotBlankとセットにすることが多いです
@Pattern(regexp="...")郵便番号、英数字のみ、などルールが厳しすぎると「正しいのに弾かれる」地獄になります。まずはゆるめ推奨です

例:郵便番号を「数字とハイフンだけ」にしたい場合(ゆるめ)

@Pattern(regexp = "^[0-9-]+$", message = "郵便番号は数字とハイフンで入力してください")
private String zip;

迷ったらこの選び方でOKです

最後に、現場での“即決ルール”だけ置いておきますね。

  • 名前・タイトル系:@NotBlank@Size(max=...)
  • メール:@NotBlank@Email
  • 数量・年齢:@Min / @Max
  • List:外側は @NotEmpty、中身は List<@Valid ...>(H2-3の型)
筆者
筆者

次は、エラーメッセージを日本語にしたり、文言を統一して運用しやすくする話に行きますね。
「message直書き → properties管理 → JSONテンプレ」の順でサクッと固めます。

エラーメッセージを日本語にする・分かりやすくする

バリデーション自体は動いたけど、「エラー文が英語っぽい」「項目ごとに言い方バラバラ」みたいになると、地味に運用がしんどいです。ここで“読みやすい日本語”に揃える型を作っちゃいましょうです。

message=””で手早く変更(まずはここ)

一番早いのは、アノテーションにそのまま書く方法です。

@NotBlank(message = "氏名は必須です")
@Size(max = 20, message = "氏名は20文字以内にしてください")
private String name;

とにかく今すぐ整えたいならこれでOKです。
ただ、増えてくると「同じ文言をあちこちで修正」が発生しやすいので、次の方法が運用向きです。

messages.propertiesで一括管理(日本語化に強い)

おすすめは messages.propertiesに文言を集めるやり方です。
DTO側は「キーを参照するだけ」にします。

@NotBlank(message = "{user.name.required}")
private String name;

@Email(message = "{user.email.invalid}")
private String email;

src/main/resources/messages.properties にこう書きます。

user.name.required=氏名は必須です
user.email.invalid=メールアドレスの形式が正しくないです

これだと、文言を変えたくなっても propertiesだけ直せば全体に反映できて楽です。
規模が大きくなったら翻訳管理ツールを検討してもいいですが、まずはpropertiesで十分戦えます。

JSONの返し方テンプレ:field別に配列で返す

フロントが扱いやすいのは、だいたいこの形です。

{
  "message": "入力内容にエラーがあります",
  "errors": {
    "name": ["氏名は必須です"],
    "email": ["メールアドレスの形式が正しくないです"]
  },
  "global": []
}

ポイントは2つです。

  • errorsfield名 → メッセージ配列(同じ項目に複数エラーがあり得るので配列が安心です)
  • 項目に紐づかないエラーは global に入れる(例:開始日 < 終了日みたいな全体ルール)

このJSON形式をチームで固定できると、フロントもバックも一気に楽になりますです。

つまずきQ&A(検索されがちな疑問を一気に解決)

ここは「みんなが同じところでコケるやつ」をまとめて潰しますね。
フォーマットは 質問 → 結論 → 理由 → 最小例 でいきます!

BindingResultって何?付けないと例外?

Q
BindingResultって何です?付けないとどうなります?
A

結論:
画面(Web)のとき、例外にせず“エラーとして受け取るための受け皿”です。付けないと、基本は例外になって400になったり、エラーページに飛んだりします。

理由:
@Valid でチェックがNGになると、Springは「入力が不正」として例外を投げます。
でも画面の場合は「同じ画面に戻してエラー表示したい」ので、例外で落ちるより BindingResultに貯めて表示が便利なんです。

最小例:

@PostMapping("/signup")
public String submit(@ModelAttribute @Valid SignupForm form,
                     BindingResult bindingResult) { // ←これが受け皿
  if (bindingResult.hasErrors()) {
    return "signup"; // 同じ画面へ
  }
  return "redirect:/signup/complete";
}

※API(JSON)ならBindingResultは基本使わず、@RestControllerAdviceで例外を拾って整形するのが王道です。

入れ子がチェックされないのはなぜ?

Q
子DTOに@NotBlankとか付けてるのに、入れ子だけチェックされません。なんで?
A

結論:
親のフィールドに @Valid が付いてないのが原因のことがほとんどです。

理由:
@Valid は「その中も見てチェックしてね」というスイッチです。
親がスイッチを押してないと、子の注釈は読まれません(=素通り)。

最小例:

public class ParentDto {
  @Valid // ←これがないとChildDtoの中が動かない
  private ChildDto child;
}

public class ChildDto {
  @NotBlank(message="子のnameは必須です")
  private String name;
}

Listも同じで、List<@Valid ItemDto> にしないと中身が見られないことがあります(H2-3の型です)。

ControllerとService、どっちでやるのが正解?

Q
バリデーションってControllerでやるのが正解ですか?Serviceですか?
A

結論:
基本は Controllerで入口チェック、必要なら Serviceでも入口チェックの二段構えが強いです。

理由:

  • Controller:外から来たリクエストを最初に止められる(APIの玄関)
  • Service:Controller以外(バッチ、別サービス、テストコードなど)から呼ばれても守れる(玄関が増えた時に事故りにくい)

最小例:

@Service
@Validated
public class UserService {
  public void register(@Valid UserDto dto) {
    // ここに来る前にチェックが走る想定
  }
}

迷ったらこの方針でOKです。

  • まずはControllerで@Valid(最優先)
  • 重要な境界(外部から呼ばれうるService)にも@Validated+@Validを追加
筆者
筆者

次はいよいよまとめです。この記事のチェックリストをもう一回ぎゅっと再掲して、「明日やること」に落としますね。

まとめ:今日から困らないための「型」を作ろう

ここまでで、Spring Bootのバリデーションで困りがちなところは一通り片付きました。

最後に「もう迷わないための型」を、チェックリストで再掲しますね。

この記事のチェックリスト再掲

困ったらこの順で見れば、だいたい直りますです。

  • spring-boot-starter-validation が入ってる?
  • importは jakarta.validation.* になってる?
  • Controller引数に @RequestBody @Valid を付けてる?
  • 入れ子DTOは、親フィールドに@Valid を付けてる?
  • Listの中身は、List<@Valid ItemDto> の形にしてる?(外側は@NotEmptyなども)
  • 画面なら BindingResult、APIなら @RestControllerAdvice でエラー返却を統一してる?
  • メッセージは message=""messages.properties日本語を揃えてる?
  • エラーJSONは field別に配列で返して、フロントが扱いやすい形にしてる?

次にやると良いこと(テスト/共通エラー形式)

明日やることを3つに絞るなら、これがおすすめです。

  • 最小テンプレを自分のプロジェクトにコピペして当てはめる
    DTO+Controller+Advice(エラーJSON)を“型”として置いちゃうのが一番早いです。
  • 入れ子/Listの付け忘れを総点検する
    親に@Valid、List要素に@Valid、外側@NotEmpty…ここが抜けると再発します。
  • エラー形式を共通クラス化してテストを足す
    「errorsはfield→配列」「globalも持つ」を固定して、テストで崩れないようにすると運用が楽になりますです。

このページの最小テンプレ(DTO+Controller+Advice)をコピペして、あなたのAPIのDTOに当てはめてみてください。

これだけで、明日からの「動かない・返せない・入れ子が無視される」が激減しますよ。

タイトルとURLをコピーしました