Spring Boot @Validatedアノテーションでグループ別バリデーションとメソッド検証を実装する方法


Spring Bootでバリデーションを書くとき、まず登場するのが@Validです。ですが、実務で少し複雑な入力チェックをやり始めると「登録と更新で必須項目を変えたい」「ControllerだけでなくServiceのメソッド引数も自動で検証したい」といった要望が出てきます。

そんなときに頼りになるのが、Springの@Validatedアノテーションです。@Validの延長線上にありつつ、グループバリデーションメソッド単位のバリデーションを自然に扱えるのが非常に便利です。

@Validatedとは何か?

@Validatedは、Springが提供するバリデーション用アノテーション(org.springframework.validation.annotation.Validated)です。役割としては「この対象にバリデーションをかける」という点で@Validと似ていますが、@Validatedバリデーショングループ(groups)を指定できるのが特徴です。

また、Springでは@Validatedをクラスに付けることで、 そのBeanのメソッド引数・戻り値を自動検証 (メソッドバリデーション) できるようになります。

@Validとの違いと使い分け

@Valid@Validatedは似ていますが、得意分野が少し違います。

  • @Valid(Jakarta標準)
    • DTOの検証を「トリガー」する用途でシンプル
    • Controllerで@RequestBodyを検証する、ネストしたオブジェクトを再帰的に検証する、などで扱いやすい
  • @Validated(Spring提供)
    • グループバリデーションを扱える(登録/更新でルールを変える、など)
    • Service層のメソッド引数や戻り値の検証に向いている

迷ったら次のイメージが分かりやすいです。

  • 「ControllerでDTOを普通に検証したい」→ まずは@Valid
  • 「検証ルールを用途別に切り替えたい」→ @Validated
  • 「Service層のメソッド境界でも検証したい」→ クラスに@Validated

グループバリデーションで登録と更新のルールを分ける

例えばユーザーの「新規作成」と「更新」で、必須項目を変えたいケースを考えてみます。

グループ用のインターフェースを用意する

グループはただのマーカーなので、空のインターフェースでOKです。

public interface OnCreate {}
public interface OnUpdate {}

DTOにgroupsを指定して制約を分ける

groups属性を使うと、どの制約をどの場面で有効にするかを切り替えられます。

public class UserRequest {

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

  @NotBlank(message = "メールアドレスは新規登録時に必須です", groups = OnCreate.class)
  @Email(message = "メールアドレスの形式が正しくありません", groups = {OnCreate.class, OnUpdate.class})
  private String email;

  // getter/setter
}

ここでのポイントは、@Validatedが「どのグループで検証するか」を決めるスイッチになっていることです。

Controllerで@Validated(グループ)を指定する

新規作成ではOnCreate、更新ではOnUpdateを指定します。

@RestController
@RequestMapping("/users")
public class UserController {

  @PostMapping
  public String create(@RequestBody @Validated(OnCreate.class) UserRequest request) {
    return "created";
  }

  @PutMapping("/{id}")
  public String update(@PathVariable Long id, @RequestBody @Validated(OnUpdate.class) UserRequest request) {
    return "updated";
  }
}

@Validではグループ指定ができないため、この切り替えは@Validatedの得意分野です。

Service層でメソッドバリデーションを有効にする

Controllerでの検証だけだと、別の経路(バッチ、イベント処理、別Controllerなど)からServiceが呼ばれたときにチェックが漏れることがあります。そこで便利なのがメソッドバリデーションです。

クラスに@Validatedを付ける

@Service
@Validated
public class UserService {

  public void register(@NotBlank(message = "名前は必須です") String name,
                       @Email(message = "メール形式が不正です") String email) {
    // 登録処理
  }
}

これだけで、register()に不正な値が渡されたときに例外が発生します。メソッドの境界で止められるので、呼び出し元が増えても安全性が上がります。

DTOを引数にする場合もOK

@Service
@Validated
public class UserService {

  public void register(@Valid UserRequest request) {
    // DTOの制約アノテーションに従って検証される
  }
}

ここは少し紛らわしいのですが、メソッドバリデーションを有効にする「スイッチ」が@Validatedで、DTOの中身を再帰的に検証する「合図」として@Validを併用する、というイメージです。

戻り値も検証できる

戻り値の制約も付けられます(例えば「必ず返す」契約を守らせたいときに便利です)。

@Service
@Validated
public class TokenService {

  public @NotBlank(message = "トークンが空です") String issueToken(@NotBlank String userId) {
    return "token";
  }
}

例外の種類とハンドリングの考え方

@Validatedを使うと、どこで検証が走ったかによって例外が変わります。よく見るのは次の2つです。

どこで失敗したかよく発生する例外典型的なケース
@RequestBodyのDTO検証MethodArgumentNotValidExceptionJSON→DTOのバリデーション(@Validated/@Validどちらでも)
メソッドバリデーションConstraintViolationExceptionServiceの引数/戻り値、@RequestParam@PathVariableの制約など

ControllerAdviceでまとめて返す例

まずは最低限、メッセージを拾って返す形の例です(プロジェクトに合わせてレスポンス形式を整えてください)。

@RestControllerAdvice
public class GlobalExceptionHandler {

  @ExceptionHandler(MethodArgumentNotValidException.class)
  public Map<String, Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
    var errors = ex.getBindingResult().getFieldErrors().stream()
        .map(err -> err.getField() + ": " + err.getDefaultMessage())
        .toList();

    return Map.of(
        "type", "validation_error",
        "errors", errors
    );
  }

  @ExceptionHandler(ConstraintViolationException.class)
  public Map<String, Object> handleConstraintViolation(ConstraintViolationException ex) {
    var errors = ex.getConstraintViolations().stream()
        .map(v -> v.getPropertyPath() + ": " + v.getMessage())
        .toList();

    return Map.of(
        "type", "constraint_violation",
        "errors", errors
    );
  }
}

@Validatedでハマりがちなポイント

メソッドバリデーションは「Spring管理のBean越し」に呼ばれて初めて効く

メソッドバリデーションは、Springの仕組み(プロキシ)で呼び出しを横取りして検証します。つまり、同じクラス内でthis.register(...)のように呼ぶと検証が走らないことがあります。

  • OK:Controller → Service(Spring管理Bean)呼び出し
  • 注意:ServiceのメソッドA → 同じServiceのメソッドB(自己呼び出し)

設計として「検証したい境界(公開API)」をServiceの外から呼ぶ形にしておくと、安全に運用できます。

グループを指定したときは「そのグループに属する制約だけ」が動く

@Validated(OnCreate.class)で検証すると、groupsを指定していない制約(デフォルトグループ)は動かないことがあります。意図的に分けたいときは便利ですが、「あれ、@NotBlankが効かないぞ?」となりやすいポイントです。

グループ運用を始めたら、DTO側の制約にgroupsを付ける方針をそろえると混乱しにくいです。

まとめ

@Validatedは、Spring Bootでバリデーションを一段階レベルアップさせたいときに便利なアノテーションです。特に、登録と更新でルールを切り替えるグループバリデーションと、Service層の境界で守りを固めるメソッドバリデーションは、実務でかなり効果的です。

まずはControllerでのグループ切り替えから導入し、必要に応じてService層にも@Validatedを広げていくと、無理なく堅牢な設計にしていけます。 ぜひプロジェクトの規模や運用に合わせて、@Valid@Validatedを使い分けてみてください。