Comprehensive Guide to Spring Validation: Simple to Advanced Usage and Implementation Principles
This article provides a detailed tutorial on Spring Validation, covering basic dependency setup, requestBody and requestParam/PathVariable validation, advanced techniques such as group, nested, collection and custom validation, programmatic validation, fail‑fast mode, the difference between @Valid and @Validated, and the underlying implementation mechanisms within Spring MVC.
Spring Validation is a second‑level wrapper around Hibernate Validator that enables automatic parameter validation in Spring MVC applications. This guide walks through both simple and advanced usage scenarios, explains the underlying implementation, and shows how to handle validation errors uniformly.
Simple Usage
Dependency Introduction
requestBody Parameter Validation
requestParam/PathVariable Parameter Validation
Unified Exception Handling
If you use Spring Boot < 2.3.x, <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.1.Final</version> </dependency> is pulled in automatically; otherwise add the dependency manually.
For POST/PUT requests, annotate the DTO with @Validated (or @Valid ) and define constraints such as @Length and @NotNull . Validation failures throw MethodArgumentNotValidException which Spring converts to a 400 response.
@Data
public class UserDTO {
private Long userId;
@NotNull
@Length(min = 2, max = 10)
private String userName;
@NotNull
@Length(min = 6, max = 20)
private String account;
@NotNull
@Length(min = 6, max = 20)
private String password;
}Controller method example:
@PostMapping("/save")
public Result saveUser(@RequestBody @Validated UserDTO userDTO) {
// business logic after successful validation
return Result.ok();
}For GET requests, you can validate individual parameters or use a DTO. The controller must be annotated with @Validated and constraints such as @Min or @Length are placed on method arguments. Validation failures raise ConstraintViolationException .
@RequestMapping("/api/user")
@RestController
@Validated
public class UserController {
@GetMapping("{userId}")
public Result detail(@PathVariable("userId") @Min(10000000000000000L) Long userId) {
// business logic
return Result.ok();
}
@GetMapping("getByAccount")
public Result getByAccount(@Length(min = 6, max = 20) @NotNull String account) {
// business logic
return Result.ok();
}
}Advanced Usage
Group Validation
Different API methods may require different validation rules for the same DTO. Define marker interfaces (e.g., Save , Update ) and assign them to constraint annotations via the groups attribute. Then specify the group in @Validated on the controller method.
@Data
public class UserDTO {
@Min(value = 10000000000000000L, groups = Update.class)
private Long userId;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String userName;
// other fields ...
public interface Save {}
public interface Update {}
} @PostMapping("/save")
public Result saveUser(@RequestBody @Validated(UserDTO.Save.class) UserDTO userDTO) {
return Result.ok();
}
@PostMapping("/update")
public Result updateUser(@RequestBody @Validated(UserDTO.Update.class) UserDTO userDTO) {
return Result.ok();
}Nested Validation
If a DTO contains another object, annotate the nested field with @Valid . Validation will cascade into the nested object, and it can be combined with group validation. Collections of nested objects are also supported.
@Data
public class UserDTO {
// ... other fields ...
@NotNull @Valid
private Job job;
@Data
public static class Job {
@Min(value = 1, groups = Update.class)
private Long jobId;
@NotNull @Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String jobName;
// ...
}
}Collection Validation
When the request body is a JSON array, wrap the list in a custom class that implements List and annotate the internal list with @Valid . This enables element‑wise validation.
public class ValidationList
implements List
{
@Delegate
@Valid
public List
list = new ArrayList<>();
@Override
public String toString() { return list.toString(); }
// other List methods delegated by Lombok
} @PostMapping("/saveList")
public Result saveList(@RequestBody @Validated(UserDTO.Save.class) ValidationList
userList) {
return Result.ok();
}Custom Validation
Create a custom constraint annotation and its validator implementation. Example: an @EncryptId annotation that validates a hexadecimal string of length 32‑256.
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Constraint(validatedBy = {EncryptIdValidator.class})
public @interface EncryptId {
String message() default "Invalid encrypted ID";
Class
[] groups() default {};
Class
[] payload() default {};
} public class EncryptIdValidator implements ConstraintValidator
{
private static final Pattern PATTERN = Pattern.compile("^[a-f\\d]{32,256}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value != null) {
return PATTERN.matcher(value).find();
}
return true;
}
}Programmatic Validation
Inject javax.validation.Validator and call validate() manually when you need explicit control.
@Autowired
private javax.validation.Validator globalValidator;
@PostMapping("/saveWithCodingValidate")
public Result saveWithCodingValidate(@RequestBody UserDTO userDTO) {
Set
> violations =
globalValidator.validate(userDTO, UserDTO.Save.class);
if (!violations.isEmpty()) {
violations.forEach(v -> System.out.println(v));
}
return Result.ok();
}Fail‑Fast Mode
Configure the validator to stop after the first constraint violation.
@Bean
public Validator validator() {
ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(true)
.buildValidatorFactory();
return factory.getValidator();
}@Valid vs @Validated
The two annotations are interchangeable for triggering validation; @Validated also supports validation groups, while @Valid does not.
Implementation Principles
requestBody Validation Mechanism
Spring MVC’s RequestResponseBodyMethodProcessor reads the request body, binds it to a DTO, and then calls validateIfApplicable() . This method scans for @Valid or @Validated annotations and invokes WebDataBinder.validate() , which ultimately delegates to Hibernate Validator.
public Object resolveArgument(...){
Object arg = readWithMessageConverters(...);
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(...);
if (arg != null) {
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors()) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
}
return adaptArgumentIfNecessary(arg, parameter);
}Method‑Level Validation Mechanism
Spring registers an AOP advisor via MethodValidationPostProcessor . The advisor applies MethodValidationInterceptor to beans annotated with @Validated . The interceptor uses ExecutableValidator from Hibernate Validator to validate method parameters and return values.
public Object invoke(MethodInvocation invocation) throws Throwable {
Class
[] groups = determineValidationGroups(invocation);
Set
> result = validator.forExecutables()
.validateParameters(invocation.getThis(), invocation.getMethod(), invocation.getArguments(), groups);
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
Object returnValue = invocation.proceed();
result = validator.forExecutables()
.validateReturnValue(invocation.getThis(), invocation.getMethod(), returnValue, groups);
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
return returnValue;
}Both requestBody and method‑level validations ultimately rely on Hibernate Validator; Spring Validation merely provides a convenient integration layer.
Java Architect Essentials
Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.
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.