Comprehensive Guide to Spring Validation: Usage, Advanced Features, and Implementation Principles
This article provides a thorough tutorial on Spring Validation, covering basic usage with requestBody and requestParam, dependency setup, group, nested, collection and custom validations, programmatic validation, fail‑fast mode, differences between @Valid and @Validated, as well as the underlying implementation mechanisms in Spring MVC and method‑level AOP validation.
Spring Validation is a wrapper around Hibernate Validator that enables automatic parameter validation in Spring MVC applications. It supports both simple and complex validation scenarios, including requestBody objects, request parameters, group validation, nested objects, collections, custom constraints, and programmatic validation.
Simple Usage
The JSR‑303 validation-api defines the Bean Validation standard, while Hibernate Validator provides the implementation with annotations such as @Email and @Length . In a Spring Boot project, adding the hibernate-validator dependency (automatically included for Spring Boot < 2.3.x) enables Spring Validation.
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.1.Final</version>
</dependency>For POST/PUT requests, DTO objects are annotated with @Validated (or @Valid ) to trigger validation. Example:
@Data
public class UserDTO {
@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;
} @PostMapping("/save")
public Result saveUser(@RequestBody @Validated UserDTO userDTO) {
// business logic executes only after successful validation
return Result.ok();
}GET requests can validate @RequestParam or @PathVariable parameters directly on method arguments, requiring the controller class to be annotated with @Validated :
@RestControllerAdvice
public class CommonExceptionHandler {
@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseStatus(HttpStatus.OK)
public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
// build error message from BindingResult
return Result.fail(BusinessCode.PARAMETER_VALIDATION_FAILED, msg);
}
@ExceptionHandler({ConstraintViolationException.class})
@ResponseStatus(HttpStatus.OK)
public Result handleConstraintViolationException(ConstraintViolationException ex) {
return Result.fail(BusinessCode.PARAMETER_VALIDATION_FAILED, ex.getMessage());
}
}Advanced Usage
Group Validation
Define validation groups to apply different constraints in different contexts (e.g., save vs. update):
@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 {}
}Apply the group when validating:
@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
When a DTO contains another object, annotate the field with @Valid to cascade validation:
@Data
public class UserDTO {
// ... other fields ...
@NotNull(groups = {Save.class, Update.class})
@Valid
private Job job;
@Data
public static class Job {
@Min(1)
private Long jobId;
@NotNull
@Length(min = 2, max = 10)
private String jobName;
@NotNull
@Length(min = 2, max = 10)
private String position;
}
}Collection Validation
To validate each element of a JSON array, wrap the collection in a custom class and annotate it with @Valid :
public class ValidationList
implements List
{
@Delegate
@Valid
public List
list = new ArrayList<>();
@Override
public String toString() { return list.toString(); }
}Controller method example:
@PostMapping("/saveList")
public Result saveList(@RequestBody @Validated(UserDTO.Save.class) ValidationList
userList) {
return Result.ok();
}Custom Validation
Create a custom annotation and validator, e.g., to validate encrypted IDs:
@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 its API directly:
@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 Hibernate 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.
Implementation Principles
requestBody Validation Mechanism
Spring MVC’s RequestResponseBodyMethodProcessor reads the request body, creates the DTO, and then calls validateIfApplicable() , which looks for @Validated or any annotation starting with "Valid" and invokes WebDataBinder.validate() . The binder delegates to Hibernate Validator.
public Object resolveArgument(MethodParameter parameter, ... ) {
Object arg = readWithMessageConverters(...);
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(...);
if (arg != null) {
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(...)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
return adaptArgumentIfNecessary(arg, parameter);
}
}Method‑Level Validation Mechanism
Spring registers a MethodValidationPostProcessor that creates an AOP advisor for beans annotated with @Validated . The advisor uses MethodValidationInterceptor to invoke ExecutableValidator.validateParameters() and validateReturnValue() , which again delegate to Hibernate Validator.
public Object invoke(MethodInvocation invocation) throws Throwable {
Class
[] groups = determineValidationGroups(invocation);
ExecutableValidator execVal = this.validator.forExecutables();
Set
> result = execVal.validateParameters(
invocation.getThis(), invocation.getMethod(), invocation.getArguments(), groups);
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
Object returnValue = invocation.proceed();
result = execVal.validateReturnValue(invocation.getThis(), invocation.getMethod(), returnValue, groups);
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
return returnValue;
}Thus, regardless of whether validation is performed on request bodies, method parameters, or return values, the core validation work is always carried out by Hibernate Validator, with Spring providing convenient integration and error handling.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn 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.