Spring BootでWebアプリケーションやREST APIを開発していると、リクエストデータのバリデーション(検証)は避けて通れない処理です。例えば、ユーザー登録時に名前やメールアドレスが空でないか、形式が正しいかといったチェックが必要になります。

そんなときに役立つのが、@Validアノテーションです。本記事では、@Validの基本的な役割から、実際のバリデーションルールの定義方法まで、実践的に解説していきます。

@Validとは何か?

@Validは、Javaの Jakarta Bean Validation(旧 JSR 303/380)仕様に準拠したバリデーション処理をトリガーするアノテーション です。Spring Bootでは、spring-boot-starter-validationを導入することで、コントローラーの引数やJavaオブジェクトに対して簡単にバリデーションが行えるようになります。

ただし、@Valid自体は「このオブジェクトを検証対象にする」という トリガー的な役割 しか持ちません。何をどう検証するかは、フィールドに付ける制約アノテーションで定義します

なぜAPI開発でよく使われるのか?

Spring BootのREST APIでは、クライアントから受け取ったJSONをPOJOにマッピングし、@Validを付けることで、Springが自動的にバリデーションを実行してくれます。

@PostMapping("/users")
public ResponseEntity<String> createUser(@RequestBody @Valid UserRequest userRequest) {
  return ResponseEntity.ok("User created");
}

リクエストが不正な場合は、SpringがMethodArgumentNotValidExceptionをスローし、適切なエラーレスポンスを返すような設計が可能です。なお @Valid@RequestBody の記述順はどちらが先でも動作は同じなので、チーム内で読みやすい方に統一すれば大丈夫です。

バリデーションの内容はアノテーションで定義する

@Validでバリデーション処理が有効になったオブジェクトには、フィールドごとに制約アノテーションを付けることで、検証内容を細かく制御 できます。

public class UserRequest {

  @NotBlank(message = "名前は必須です")
  @Size(min = 2, max = 20, message = "名前は2〜20文字で入力してください")
  private String name;

  @Email(message = "メールアドレスの形式が正しくありません")
  private String email;
}

このように、フィールドごとに複数の制約を組み合わせることも可能です。message属性を指定すれば、独自のエラーメッセージも定義できます。

よく使われる制約アノテーション

対象型と典型的なコード例を早見表として整理します。

アノテーション対象型コード例
@NotNull任意@NotNull private Long id;
@NotBlankString@NotBlank private String name;
@NotEmptyString / Collection / 配列@NotEmpty private List<String> tags;
@Size(min, max)String / Collection / 配列@Size(min=2, max=20) private String name;
@EmailString@Email private String email;
@Pattern(regexp)String@Pattern(regexp="\\d{3}-\\d{4}") private String zip;
@Min / @Max数値@Min(0) @Max(120) private int age;
@Positive / @Negative数値@Positive private BigDecimal price;
@Past / @Future日付@Past private LocalDate birthday;

Hibernate Validator 独自拡張として @URL @Length @Range などもあり、spring-boot-starter-validation 経由で利用できます。org.hibernate.validator.constraints パッケージから import してください。

なお Spring Boot 3.x では import パスが jakarta.validation.constraints.* になっています。2.x 系の javax.validation.constraints.* から変更されているので注意してください。

迷いやすいのが @NotNull @NotEmpty @NotBlank の使い分けです。@NotNull は null だけを弾くので空文字 "" は通ります。@NotEmpty は null と長さ0を弾きますが、空白のみの " " は通ります。文字列フィールドには、null・空・空白のみをすべて弾く @NotBlank を使うのが基本です。

ネストされたオブジェクトの検証

@Validは、ネストされたオブジェクトにも再帰的にバリデーションを適用できます。

public class OrderRequest {

  @Valid
  private Address address;
}

この場合、Addressクラス内に定義されたバリデーションルールも適用されます。逆に言うと、親DTOをコントローラーで検証していても、子DTOのフィールドに@Validが無いと再帰検証されません。入れ子構造では親子両方の注釈を確認してください。

サービス層での手動バリデーション

コントローラー以外のクラス(たとえばService層)でも、Validatorを使えばバリデーションを明示的に実行できます。

@Service
public class UserService {

  private final Validator validator;

  public UserService(Validator validator) {
    this.validator = validator;
  }

  public void register(UserRequest request) {
    Set<ConstraintViolation<UserRequest>> violations = validator.validate(request);
    if (!violations.isEmpty()) {
      throw new IllegalArgumentException("Validation failed: " + violations);
    }
    // 登録処理
  }
}

@Validatedとの違いと使い分け

@Validated は Spring Framework 独自のアノテーションで、@Valid にはない グループ指定クラスに付与してメソッド引数を自動検証する機能 を担います。@Valid を引数に付けるだけではメソッド検証は起動しない点に注意してください。グループ設計の考え方や Service 層でのメソッド検証は、続編の Spring Boot @Validatedアノテーションでグループ別バリデーションとメソッド検証を実装する方法 で掘り下げています。

グループバリデーションの最小例

@Validated を使うと、登録時と更新時で必須項目を切り替えるといったグループ別の検証が可能です。

public interface OnCreate {}
public interface OnUpdate {}

public class UserRequest {
  @Null(groups = OnCreate.class)
  @NotNull(groups = OnUpdate.class)
  private Long id;

  @NotBlank(groups = {OnCreate.class, OnUpdate.class})
  private String name;
}

@PostMapping("/users")
public ResponseEntity<?> create(@RequestBody @Validated(OnCreate.class) UserRequest req) {
  return ResponseEntity.ok().build();
}

@Valid ではグループ指定ができないため、登録/更新で挙動を変えたい場合は @Validated(グループ.class) を使います。

@Valid と @Validated の比較表

両者は似ていますが、使える場所と機能に違いがあります。実務で迷ったときの早見表として整理します。

観点@Valid (Jakarta)@Validated (Spring)
由来Jakarta Bean Validation 仕様Spring Framework 独自
主な用途コントローラー引数・ネストDTOの再帰検証グループバリデーション、メソッド引数検証
グループ指定不可可能(@Validated(OnCreate.class) など)
例外型MethodArgumentNotValidExceptionConstraintViolationException
クラスに付与不可(フィールド/引数のみ)可能(Bean全体の検証を有効化)
ネスト検証子DTOフィールドへの再帰適用が直感的ネストでは @Valid を併用するのが一般的

端的にまとめると、コントローラーの@RequestBodyに対する標準的な検証は@Valid、Service層のメソッド引数検証や登録/更新でルールを切り替えたい場合は@Validated という使い分けが実務上もっとも事故が少ないです。

エラーレスポンスを標準化する

@Validを使うなら、MethodArgumentNotValidExceptionの返却形式を最初に標準化しておくのが重要です。ここが毎回バラバラだと、フロントエンド側の実装コストが増えます。

@RestControllerAdvice
public class ApiExceptionHandler {

  @ExceptionHandler(MethodArgumentNotValidException.class)
  public ResponseEntity<Map<String, Object>> handleValidation(MethodArgumentNotValidException ex) {
    var errors = ex.getBindingResult().getFieldErrors().stream()
        .map(error -> Map.of(
            "field", error.getField(),
            "message", error.getDefaultMessage()
        ))
        .toList();

    return ResponseEntity.badRequest().body(Map.of(
        "code", "VALIDATION_ERROR",
        "errors", errors
    ));
  }
}

このようにレスポンスのキー(code, errors, field, message)を固定すると、利用側の扱いが安定します。他の例外も含めた @RestControllerAdvice の設計全体は Spring BootのREST APIで統一的なエラーレスポンスを返す方法 を参照してください。

Spring Boot 3 なら、application.propertiesspring.mvc.problemdetails.enabled=true を設定するだけで、RFC 9457 準拠の application/problem+json 形式で返す方法もあります。ただしデフォルトではフィールド単位のエラー一覧が含まれないため、実務での拡張方法は Spring Boot 3.xのProblem Details(RFC 9457)でエラーレスポンスを標準化する方法 にまとめています。

よくある落とし穴

spring-boot-starter-validation の依存を入れ忘れている

Spring Boot 2.3 以降、spring-boot-starter-web にはバリデーション関連の依存が含まれていません。@Valid を付けても何も起きない場合は、まず依存を確認してください。

// build.gradle
dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-web'
  implementation 'org.springframework.boot:spring-boot-starter-validation'
}
<!-- pom.xml -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

バージョンは Spring Boot の BOM が管理するので個別指定は不要です。Starter の仕組みは Spring Boot Starterって何?、2.x から上げる際の javaxjakarta 置換の手順は Spring Boot 2.x から 3.x への移行ガイド を参照してください。

@RequestParam / @PathVariable に制約を付けても検証されない

@Valid は Bean(オブジェクト)を対象にするため、@RequestParam@PathVariable のような単項目には効きません。単項目を検証するには、メソッド引数に制約アノテーションを直接付けます。

@RestController
@Validated
public class UserController {

  @GetMapping("/users/{id}")
  public UserResponse find(@PathVariable @Min(1) Long id,
                           @RequestParam(required = false) @Size(max = 20) String keyword) {
    // ...
  }
}

Spring Boot 3.2(Spring Framework 6.1)以降は、コントローラーのメソッド引数に制約が付いていれば クラスに @Validated を付けなくても Spring MVC が組み込みでメソッド検証を行い、失敗時は HandlerMethodValidationException を投げます。クラスに @Validated を付けた場合は従来どおり AOP ベースの検証となり ConstraintViolationException になります。どちらの例外が飛ぶかは @RestControllerAdvice のハンドラ設計に直結するので、使っているバージョンで一度確認しておくと安全です。

グループを指定すると Default グループの制約が走らない

@Validated(OnCreate.class) のようにグループを指定すると、グループ未指定の制約(暗黙の Default グループ)は検証されなくなります。「@NotBlank を付けているのに登録 API だけ素通りする」というときは大抵これが原因です。既存の制約も併せて実行したい場合は複数グループを指定するか、グループインターフェースに Default を継承させます。

import jakarta.validation.groups.Default;

// Default を継承しておくと OnCreate 指定時に既存の制約も一緒に検証される
public interface OnCreate extends Default {}

@PostMapping("/users")
public ResponseEntity<?> create(@RequestBody @Validated(OnCreate.class) UserRequest req) {
  return ResponseEntity.ok().build();
}

// もしくはその場で複数指定する
// @Validated({OnCreate.class, Default.class})

検証順序を制御したい場合は @GroupSequence を使います。先のグループで違反があると後続は評価されないため、「形式チェックが通ってから重い制約を実行する」用途に向いています。

@GroupSequence({Default.class, OnCreate.class})
public interface CreateSequence {}

// @Validated(CreateSequence.class) で Default → OnCreate の順に検証

ネストした DTO にグループが伝播しない

@Valid を付けたネストフィールドは、親と 同じグループ で検証されます。子 DTO では別のグループ(または Default)で検証したい場合は @ConvertGroup でグループを変換します。

public class OrderRequest {

  @Valid
  @ConvertGroup(from = OnCreate.class, to = Default.class)
  private Address address;
}

@NotNull をプリミティブ型に付けている

intboolean などのプリミティブ型は null になれないため、@NotNull を付けても意味がありません。JSON でフィールドが省略されると 0false がデフォルト値として入り、検証をすり抜けます。「未指定を弾きたい」なら ラッパー型 + @NotNull に変えてください。

// NG: 省略されると 0 が入り、@NotNull は常に通る
@NotNull
private int age;

// OK: 省略されると null になり、@NotNull で弾ける
@NotNull
@Min(0)
private Integer age;

補足として、@RequestParam はデフォルトで required = true なので、未指定なら MissingServletRequestParameterException(400)になります。required = false にした上で「指定されたなら値の制約を検証したい」というケースが、単項目に制約を付ける典型的な場面です。また Optional<String> のような型は、Optional<@NotBlank String> のように 型引数側に制約を書く と中身を検証できます。

バリデーションは通るのに業務要件を満たさない

@NotBlank@Email が通っても、それは「値の形が正しい」ことしか保証しません。「同じメールアドレスが既に存在するか」のようにDB照会が必要なルールはService層でチェックします。一方「開始日は終了日より前」のような相関チェックは、毎回 Service 層に書くより カスタムバリデーションアノテーション として ConstraintValidator に切り出すと再利用できます。

バリデーションメッセージをハードコードしすぎる

メッセージ文言を Java コードに直書きすると、文言変更のたびに再ビルドが必要になり、多言語対応もできません。将来的に多言語対応する可能性がある場合、messages.properties へ寄せる設計が有効です。

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

キーは validation.name.required=名前は必須です のように定義し、Spring Boot が LocaleResolver で解決した言語に応じて自動で切り替えてくれます。MessageSource の設定手順は Spring Bootで多言語対応(i18n)を実装する方法 にまとめています。

フォーム入力とテストでの扱い

REST API ではなく Thymeleaf などの HTML フォームの場合は、@ModelAttribute の引数に @Valid を付け、直後に BindingResult を受け取る ことで例外を投げずにエラーを画面へ戻せます。

@PostMapping("/users")
public String create(@Valid @ModelAttribute("form") UserForm form, BindingResult result) {
  if (result.hasErrors()) {
    return "users/new"; // 入力画面を再表示
  }
  userService.register(form);
  return "redirect:/users";
}

BindingResult は検証対象の引数の すぐ次 に置く必要があります。離すと MethodArgumentNotValidException が投げられます。テンプレート側では th:errors="*{name}" のようにフィールド単位でメッセージを表示できます。

バリデーションの挙動は @WebMvcTest + MockMvc でテストしておくと安心です。不正な JSON を送って 400 とレスポンスの中身を検証します。@RestControllerAdvice もスキャン対象なので、標準化したレスポンス形式ごとテストできます。

@WebMvcTest(UserController.class)
class UserControllerTest {

  @Autowired
  MockMvc mockMvc;

  @Test
  void 名前が空なら400を返す() throws Exception {
    mockMvc.perform(post("/users")
            .contentType(MediaType.APPLICATION_JSON)
            .content("{\"name\":\"\",\"email\":\"[email protected]\"}"))
        .andExpect(status().isBadRequest())
        .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"))
        .andExpect(jsonPath("$.errors[0].field").value("name"));
  }
}

MockMvc の基本は Spring BootでMockMvcを使ったControllerの単体テストを書く方法 を参照してください。

まとめ

@Validアノテーションを使うと、Spring Bootの入力値バリデーションを簡潔かつ柔軟に実装できます。検証ルールは対象フィールドの制約アノテーションで定義し、エラーレスポンスの形式を最初に標準化しておくのがポイントです。グループ指定やメソッド検証が必要になったら@Validatedと使い分けて、堅牢なAPIを作っていきましょう。

関連記事