Mastering @Valid and @Validated: Practical Group Validation in Spring
This article explains the fundamental differences between Spring's @Valid and @Validated annotations, demonstrates how to apply group validation, nested object checks, and custom validation annotations, and provides a complete strategy for global exception handling and production‑grade best practices.
1. Difference Between @Valid and @Validated
@Validis the original JSR‑303 annotation from the javax.validation package; it only supports basic field validation and recursive validation of nested objects. It cannot perform group validation and can be placed only on method parameters or fields. @Validated is Spring's extension in the org.springframework.validation package. It adds native support for validation groups, can be placed on controller classes or methods, but does not trigger recursive validation of nested objects unless those fields are also annotated with @Valid.
1.1 Origin and Capability
@Validbelongs to the JSR‑303 standard, while @Validated belongs to Spring’s validation package. They serve different purposes and should not be mixed indiscriminately.
1.2 Recommended Usage in Production
Controller methods should declare @Validated(Group.class) to enable group validation, and any nested VO fields that require recursive checks must be annotated with @Valid. This combination is the standard practice in most projects.
1.3 Common Built‑in Validation Annotations
@NotBlank: string must not be empty after trimming. @NotNull: object must not be null (suitable for numbers, dates, etc.). @NotEmpty: collection, array or string length must be greater than zero. @Min/@Max: numeric range limits. @Pattern: regex match (e.g., phone, ID, email). @Size: length range for strings or collections.
2. Basic Validation Without Groups
2.1 Adding Dependency
<!-- Spring Boot 2.x includes validation automatically -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Spring Boot 3.x requires explicit Jakarta validation dependency -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>2.2 Basic VO Definition
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import lombok.Data;
@Data
public class UserVO {
@NotNull(message = "User ID cannot be null")
private Long id;
@NotBlank(message = "Username cannot be blank")
private String username;
@NotBlank(message = "Phone cannot be blank")
private String phone;
}2.3 Controller Validation Example
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/user")
@Validated
public class UserController {
@PostMapping("/edit")
public Result editUser(@RequestBody @Valid UserVO userVO) {
// Business logic runs only after validation passes
return Result.success();
}
}Using only @Valid shares a single rule set for all scenarios, which is suitable for simple, single‑purpose endpoints.
3. Group Validation Code Samples
3.1 Defining Group Marker Interfaces
// Add scenario
public interface AddGroup {}
// Update scenario
public interface UpdateGroup {}3.2 VO with Group‑Specific Rules
import javax.validation.constraints.*;
import lombok.Data;
@Data
public class UserVO {
// Only validated in the Update group
@NotNull(message = "Update requires user ID", groups = UpdateGroup.class)
private Long id;
// Validated in both Add and Update groups
@NotBlank(message = "Username cannot be blank", groups = {AddGroup.class, UpdateGroup.class})
private String username;
@NotBlank(message = "Phone cannot be blank", groups = {AddGroup.class, UpdateGroup.class})
private String phone;
}3.3 Controller Selecting a Group
@RestController
@RequestMapping("/user")
public class UserController {
// Add endpoint: only AddGroup rules apply
@PostMapping("/add")
public Result addUser(@RequestBody @Validated(AddGroup.class) UserVO userVO) {
return Result.success();
}
// Update endpoint: only UpdateGroup rules apply
@PostMapping("/update")
public Result updateUser(@RequestBody @Validated(UpdateGroup.class) UserVO userVO) {
return Result.success();
}
}The add endpoint accepts a payload without id and passes validation, while the update endpoint rejects a missing id, cleanly separating business rules without duplicating VO classes.
3.4 Multiple Groups Simultaneously
When an interface needs both Add and Query rules, multiple group classes can be passed:
@PostMapping("/query")
public Result query(@RequestBody @Validated({AddGroup.class, QueryGroup.class}) UserVO vo) {
return Result.success();
}4. Nested Object Group Validation (Recursive VO Validation)
Spring’s @Validated alone does not recurse into nested objects. Adding @Valid on the nested field triggers recursive validation.
4.1 Child VO
@Data
public class AddressVO {
@NotBlank(message = "Province cannot be blank", groups = {AddGroup.class, UpdateGroup.class})
private String province;
@NotBlank(message = "City cannot be blank", groups = {AddGroup.class, UpdateGroup.class})
private String city;
}4.2 Parent VO with Nested Object
@Data
public class UserVO {
@NotNull(message = "Update requires ID", groups = UpdateGroup.class)
private Long id;
@NotBlank(message = "Username cannot be blank", groups = {AddGroup.class, UpdateGroup.class})
private String username;
// Enable recursive validation
@Valid
private AddressVO address;
}4.3 Collection Nested Validation
Collections also require @Valid on the field to validate each element:
@Valid
@NotEmpty(message = "Address list cannot be empty", groups = AddGroup.class)
private List<AddressVO> addressList;5. Global Exception Handling for Validation Errors
Validation failures throw MethodArgumentNotValidException for JSON bodies and BindException for GET/form parameters. A @RestControllerAdvice can capture both and return a unified error response.
import org.springframework.validation.*;
import org.springframework.web.bind.*;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestControllerAdvice
public class GlobalValidExceptionHandler {
// JSON body validation errors
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> handleValidException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().get(0).getDefaultMessage();
return Result.fail(400, "Parameter validation failed: " + message);
}
// GET/form validation errors
@ExceptionHandler(BindException.class)
public Result<?> handleBindException(BindException e) {
String message = e.getBindingResult().getFieldErrors().get(0).getDefaultMessage();
return Result.fail(400, "Parameter validation failed: " + message);
}
}6. Custom Validation Annotation
When built‑in constraints cannot cover business rules (e.g., phone number format), a custom annotation with its validator can be created and used with groups.
6.1 Annotation Definition
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface PhoneValid {
String message() default "Invalid phone format";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}6.2 Validator Implementation
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;
public class PhoneValidator implements ConstraintValidator<PhoneValid, String> {
private static final String PHONE_REG = "^1[3-9]\\d{9}$";
private Pattern pattern;
@Override
public void initialize(PhoneValid constraintAnnotation) {
pattern = Pattern.compile(PHONE_REG);
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.isEmpty()) {
return true; // let @NotBlank handle emptiness if needed
}
return pattern.matcher(value).matches();
}
}6.3 Using Custom Annotation with Groups
@PhoneValid(groups = {AddGroup.class, UpdateGroup.class})
private String phone;7. Common Pitfalls and Solutions
Group validation does not work when the controller only uses @Valid. Use @Validated(Group.class) on method parameters.
Nested object validation fails if @Valid is missing on the nested field. Add @Valid to every nested VO or collection field.
Id field is validated in the add scenario because its annotation lacks a group restriction. Bind the annotation to UpdateGroup only.
GET request validation errors are not caught because only MethodArgumentNotValidException is handled. Add a handler for BindException.
Custom annotation groups are ignored if the annotation does not declare a groups() attribute. Include the attribute with a default empty array.
Placing @Validated on the controller class without specifying a group causes all endpoints to share the same validation set, overriding method‑level groups. Keep class‑level @Validated without a group and specify groups on each method.
8. Production‑Grade Standardization
Combine @Validated(Group) on method parameters with @Valid on nested fields for full group and recursive validation.
Define separate group interfaces (AddGroup, UpdateGroup, QueryGroup) to let a single VO serve multiple scenarios.
Provide custom messages for every constraint; avoid default English messages.
Force @Valid on all nested VO and collection fields to prevent silent validation loss.
Implement a global exception handler that returns a unified error code and message for both JSON and form validation failures.
Encapsulate complex business rules in custom annotations to reduce repetitive if‑checks in service layers.
Eliminate manual null‑checks in services; rely on the validation layer to filter illegal input early.
9. Full Summary
@Validhandles recursive validation, while @Validated provides group capabilities. Using them together forms the standard solution for Spring MVC parameter validation. Group validation enables a single VO to support add, edit, and query scenarios, dramatically reducing duplicated request objects and boilerplate null‑checks. Mastering basic constraints, nested validation, custom annotations, and a unified global exception handler yields a clean, maintainable, and interview‑ready validation architecture.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Tech Workshop
Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
