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検証 | MethodArgumentNotValidException | JSON→DTOのバリデーション(@Validated/@Validどちらでも) |
| メソッドバリデーション | ConstraintViolationException | Serviceの引数/戻り値、@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を使い分けてみてください。