Comprehensive Guide to Spring Validation: Best Practices, Scenarios, and Advanced Features
This article provides an in‑depth tutorial on Spring Validation, covering basic usage, dependency configuration, requestBody and requestParam validation, unified exception handling, group and nested validation, collection checks, custom constraints, programmatic validation, fail‑fast mode, and the differences between @Valid and @Validated.
Spring Validation is a powerful mechanism built on the JSR‑303 Bean Validation API (validation‑api) and implemented by Hibernate Validator. It adds annotations such as @Email and @Length and integrates with Spring MVC to automatically validate controller method parameters.
Simple Usage
The spring-boot-starter-web starter pulls in hibernate-validator automatically for Spring Boot versions below 2.3.x. For newer versions you must add the dependency manually:
org.hibernate
hibernate-validator
6.0.1.FinalFor web services, validation should be performed in the Controller layer. POST/PUT requests use a request body, while GET requests use @RequestParam or @PathVariable .
RequestBody Parameter Validation
Define a DTO and annotate it with @Validated (or @Valid ) on the controller method:
@PostMapping("/save")
public Result saveUser(@RequestBody @Validated UserDTO userDTO) {
// business logic executes only after successful validation
return Result.ok();
}If validation fails, Spring throws MethodArgumentNotValidException and returns a 400 Bad Request response by default.
RequestParam / PathVariable Validation
Annotate the controller class with @Validated and place constraint annotations on the method parameters:
@RestControllerAdvice
public class CommonExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
BindingResult bindingResult = ex.getBindingResult();
StringBuilder sb = new StringBuilder("Validation failed: ");
for (FieldError fieldError : bindingResult.getFieldErrors()) {
sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
}
return Result.fail(BusinessCode.PARAMETER_VALIDATION_FAILED, sb.toString());
}
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public Result handleConstraintViolationException(ConstraintViolationException ex) {
return Result.fail(BusinessCode.PARAMETER_VALIDATION_FAILED, ex.getMessage());
}
}Example of validating a path variable:
@GetMapping("{userId}")
public Result detail(@PathVariable("userId") @Min(10000000000000000L) Long userId) {
// logic after successful validation
UserDTO userDTO = new UserDTO();
userDTO.setUserId(userId);
return Result.ok(userDTO);
}Advanced Usage
Group Validation
Define validation groups to apply different rules for save and update operations:
@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 in the controller:
@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 nested field with @Valid to trigger cascade validation:
@Data
public class UserDTO {
@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String userName;
@Valid
private Job job;
@Data
public static class Job {
@Min(value = 1, groups = Update.class)
private Long jobId;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String jobName;
}
}Collection Validation
Wrap a collection in a custom class and annotate the list with @Valid to validate each element:
public class ValidationList
implements List
{
@Delegate
@Valid
public List
list = new ArrayList<>();
@Override
public String toString() {
return list.toString();
}
}
@PostMapping("/saveList")
public Result saveList(@RequestBody @Validated(UserDTO.Save.class) ValidationList
userList) {
return Result.ok();
}Custom Constraint
Create a custom annotation and validator to check encrypted IDs (32‑256 characters, digits or a‑f):
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@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 invoke it manually:
@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 at the first violation:
@Bean
public Validator validator() {
ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(true)
.buildValidatorFactory();
return factory.getValidator();
}@Valid vs @Validated
Aspect
@Valid
@Validated
Provider
JSR‑303 (Bean Validation)
Spring
Group support
No
Yes
Target locations
METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE
TYPE, METHOD, PARAMETER
Nested validation
Supported
Not supported
Implementation Principles
Spring MVC’s RequestResponseBodyMethodProcessor resolves method arguments, creates a WebDataBinder , and calls validateIfApplicable . This method scans for @Validated or any annotation whose name starts with “Valid” and triggers binder.validate() , which delegates to Hibernate Validator.
Method‑level validation is implemented via AOP. MethodValidationPostProcessor registers an advisor for beans annotated with @Validated . The advisor uses MethodValidationInterceptor , which invokes ExecutableValidator.validateParameters and validateReturnValue from Hibernate Validator, throwing ConstraintViolationException on failure.
Thus, both request‑body validation and method‑level validation ultimately rely on Hibernate Validator; Spring merely provides the integration layer.
Architecture Digest
Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.
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.