When you write validation in Spring Boot, the first annotation you encounter is @Valid. But once you start implementing slightly more complex input checks in real projects, requests like “I want different required fields for create and update” or “I want Service method arguments validated automatically, not just Controllers” start to come up.
That’s where Spring’s @Validated annotation comes in. It builds on @Valid while letting you handle group validation and method-level validation naturally, which is extremely convenient.
What Is @Validated?
@Validated is a validation annotation provided by Spring (org.springframework.validation.annotation.Validated). Its role is similar to @Valid in that it says “apply validation to this target,” but what sets @Validated apart is that it lets you specify validation groups.
In addition, when you put @Validated on a class in Spring, the method arguments and return values of that Bean are validated automatically (method validation).
Required Dependencies and Version Assumptions
The code in this article assumes Spring Boot 3.x (Java 17 or later). @Validated itself is part of Spring Framework, but to use constraint annotations such as @NotBlank and @Email, you need to add spring-boot-starter-validation. Note that spring-boot-starter-web alone does not pull it in.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
}
Adding this Starter brings in Hibernate Validator, the Jakarta Bean Validation implementation, and Spring Boot auto-configures LocalValidatorFactoryBean and MethodValidationPostProcessor for you. In other words, with a single added dependency, both DTO validation in Controllers and method validation via class-level @Validated are ready to work. For how Starters bundle dependencies together, see What Is a Spring Boot Starter?.
In Spring Boot 3.x, the constraint annotations live in the jakarta.validation package, not javax.validation. If you upgraded from 2.x and your import statements no longer resolve, this missed replacement is the first thing to suspect. The migration steps are covered in Spring Boot 2.x to 3.x Migration Guide.
Differences from @Valid and When to Use Each
@Valid and @Validated look alike, but their strengths differ slightly.
@Valid(Jakarta standard)- Simple, used to “trigger” DTO validation
- Easy to use for validating
@RequestBodyin Controllers, recursively validating nested objects, and so on
@Validated(provided by Spring)- Supports group validation (e.g., different rules for create vs. update)
- Well suited for validating method arguments and return values in the Service layer
If you’re unsure, this mental model helps:
- “I just want to validate a DTO in a Controller” → start with
@Valid - “I want to switch validation rules by use case” →
@Validated - “I want validation at Service-layer method boundaries too” →
@Validatedon the class
Here’s a table summarizing the differences.
| Aspect | @Valid | @Validated |
|---|---|---|
| Provided by | Jakarta Bean Validation (jakarta.validation.Valid) | Spring Framework (org.springframework.validation.annotation.Validated) |
| Group specification | Not supported | Supported, e.g. @Validated(OnCreate.class) |
| Recursive validation of nested objects | Can be placed on a field to validate recursively | Cannot be placed on fields (use @Valid to indicate recursion) |
| Enabling method validation | Not supported | Placing it on a class validates that Bean’s arguments and return values |
| Where it can be placed | Method parameters, fields, constructor parameters, etc. | Classes, methods, method parameters |
Exception when @RequestBody validation fails | MethodArgumentNotValidException | MethodArgumentNotValidException |
If you put both @Valid and @Validated on a Controller argument, Spring scans the argument’s annotations in order and validates only once, using the hint from the first @Valid-style annotation it finds. There’s no double validation, but which group setting gets used depends on the annotation order, so stick to just one of them on any given argument.
For the basics of using @Valid on its own and for recursively validating nested DTOs, see How to Implement Validation Simply with the Spring Boot @Valid Annotation.
Separating Create and Update Rules with Group Validation
Consider a case where you want different required fields for “creating” and “updating” a user.
Define Interfaces for the Groups
Groups are just markers, so empty interfaces are fine.
public interface OnCreate {}
public interface OnUpdate {}
Assign groups to Constraints in the DTO
The groups attribute lets you switch which constraints are active in which situation.
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
}
The key point here is that @Validated acts as the switch that decides “which group to validate with.”
Specify @Validated(group) in the Controller
Use OnCreate for creation and OnUpdate for updates.
@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";
}
}
Since @Valid cannot specify groups, this kind of switching is exactly what @Validated excels at.
Enabling Method Validation in the Service Layer
If you only validate in Controllers, checks can be skipped when the Service is called through other paths (batch jobs, event handlers, other Controllers, and so on). This is where method validation comes in handy.
Put @Validated on the Class
@Service
@Validated
public class UserService {
public void register(@NotBlank(message = "名前は必須です") String name,
@Email(message = "メール形式が不正です") String email) {
// 登録処理
}
}
That’s all it takes: an exception is thrown when invalid values are passed to register(). Because the check happens at the method boundary, safety improves even as the number of callers grows.
DTO Arguments Work Too
@Service
@Validated
public class UserService {
public void register(@Valid UserRequest request) {
// DTOの制約アノテーションに従って検証される
}
}
This part is a little confusing, but think of it this way: @Validated is the “switch” that enables method validation, and @Valid is used alongside it as the “signal” to recursively validate the contents of the DTO.
Return Values Can Be Validated Too
You can also put constraints on return values (useful, for example, when you want to enforce a contract that a method always returns something).
@Service
@Validated
public class TokenService {
public @NotBlank(message = "トークンが空です") String issueToken(@NotBlank String userId) {
return "token";
}
}
Exception Types and How to Handle Them
With @Validated, the exception you get depends on where validation ran. The two you’ll see most often are:
| Where it failed | Common exception | Typical case |
|---|---|---|
DTO validation of @RequestBody | MethodArgumentNotValidException | JSON → DTO validation (with either @Validated or @Valid) |
| Method validation | ConstraintViolationException | Service arguments/return values, constraints on @RequestParam or @PathVariable, etc. |
Handling Them Together with ControllerAdvice
Here’s a minimal example that collects the messages and returns them (adjust the response format to suit your project).
@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
);
}
}
Common Pitfalls with @Validated
The Exception for Controller Argument Validation Changes in Spring Boot 3.2 and Later
Starting with Spring Boot 3.2 (Spring Framework 6.1), constraints placed directly on Controller method arguments, such as those on @RequestParam or @PathVariable, are validated by Spring MVC’s built-in method validation. When validation fails through this path, a HandlerMethodValidationException is thrown instead of the traditional ConstraintViolationException.
@GetMapping("/users")
public List<UserResponse> search(@RequestParam @Size(min = 2) String keyword) {
// Spring Boot 3.2以降は失敗すると HandlerMethodValidationException になる
return List.of();
}
This built-in validation is active only when the Controller class does not have @Validated on it. If you put @Validated on the class, the traditional proxy-based method validation takes precedence and ConstraintViolationException is thrown. If your existing @RestControllerAdvice only catches ConstraintViolationException, after upgrading your custom handler will be bypassed and Spring’s standard 400 response will be returned instead, leaving your error formats inconsistent. On 3.2 and later, add a handler for HandlerMethodValidationException as well.
@ExceptionHandler(HandlerMethodValidationException.class)
public Map<String, Object> handleHandlerMethodValidation(HandlerMethodValidationException ex) {
var errors = ex.getAllErrors().stream()
.map(MessageSourceResolvable::getDefaultMessage)
.toList();
return Map.of(
"type", "validation_error",
"errors", errors
);
}
Method Validation Only Works When Called Through a Spring-Managed Bean
Method validation works by intercepting calls through Spring’s mechanism (proxies). This means that if you call a method within the same class, like this.register(...), validation may not run.
- OK: Controller → Service (Spring-managed Bean) call
- Caution: Service method A → method B in the same Service (self-invocation)
Designing things so that the “boundary you want validated (the public API)” is called from outside the Service keeps things working safely.
When You Specify a Group, Only Constraints Belonging to That Group Run
When validating with @Validated(OnCreate.class), constraints without a groups attribute (the default group) may not run. This is convenient when you intentionally want to separate them, but it’s also a common source of “wait, why isn’t @NotBlank working?” moments.
Once you start using groups, adopting a consistent policy of adding groups to all constraints in the DTO helps avoid confusion.
Controlling Validation Order with Group Sequence
For cases where you want “heavier validation to run only if the required-field checks pass,” @GroupSequence is effective. For example, if a custom constraint that hits the database runs even when the blank check has already failed, you end up with unnecessary extra error messages and wasted processing time.
Define a Group That Represents the Order
public interface BasicChecks {}
public interface BusinessChecks {}
@GroupSequence({BasicChecks.class, BusinessChecks.class})
public interface OrderedChecks {}
OrderedChecks itself holds no constraints; it’s just an ordering. The constraints in BusinessChecks are evaluated only when all constraints in BasicChecks pass.
Assign Constraints to Each Group in the DTO
public class OrderRequest {
@NotBlank(message = "商品コードは必須です", groups = BasicChecks.class)
private String productCode;
@NotNull(message = "数量は必須です", groups = BasicChecks.class)
@Min(value = 1, message = "数量は1以上で指定してください", groups = BusinessChecks.class)
private Integer quantity;
// getter/setter
}
Specify the Sequence Group in the Controller
On the Controller side, specify the interface annotated with @GroupSequence rather than the individual groups.
@PostMapping("/orders")
public String create(@RequestBody @Validated(OrderedChecks.class) OrderRequest request) {
return "accepted";
}
If a request comes in with productCode empty, validation stops at the BasicChecks stage, so the @Min message is not returned. BusinessChecks runs for the first time on the next request, once the required fields are filled in. Since errors come back in stages, the frontend display stays clean too.
Watch Out for the Relationship with the Default Group
Constraints without groups belong to the Default group (jakarta.validation.groups.Default). If Default.class is not included in the OrderedChecks sequence, constraints without groups won’t run at all. When adding a sequence to an existing DTO after the fact, it’s safest to put Default.class first, like this:
@GroupSequence({Default.class, BusinessChecks.class})
public interface OrderedChecks {}
Note that you can also put @GroupSequence on the DTO class itself to redefine the order of that class’s default group. However, in that case the sequence must include the class itself, which makes it a bit harder to read, so I recommend starting with the interface-based ordering shown above.
Standardizing the Response Format in Practice
When @Validated on Controllers and @Validated on Services coexist, the response format tends to drift between exception types.
For API operations, aligning on the following policy makes things easier to manage:
- Use a common response format regardless of exception type
- Use
codeanderrorsas fixed keys - Include
path(the field) andmessage(the description) in each entry oferrors
This alone significantly reduces implementation cost for the frontend and for integrations with other services.
Summary
@Validated is a convenient annotation for taking validation in Spring Boot up a level. In particular, group validation for switching rules between create and update, and method validation for hardening the boundaries of the Service layer, are highly effective in real-world projects.
Start by introducing group switching in Controllers, then extend @Validated to the Service layer as needed, and you’ll arrive at a robust design without straining.
Try using @Valid and @Validated according to your project’s scale and operational needs.