Master Spring Validation: From Simple Use to Advanced Techniques and Internals
This guide thoroughly explores Spring Validation, covering basic and advanced usage such as requestBody and requestParam checks, grouping, nested and collection validation, custom constraints, programmatic validation, fail-fast configuration, and the underlying implementation mechanisms within Spring MVC and Hibernate Validator.
Java's Bean Validation (JSR‑303) defines the validation API, while Hibernate Validator provides the implementation and annotations such as @Email, @Length.
Spring Validation wraps Hibernate Validator to enable automatic validation of Spring MVC controller parameters. The following examples use a Spring Boot project.
Simple Usage
Dependency introduction – for Spring Boot < 2.3.x the starter brings in hibernate‑validator automatically; for newer versions add the dependency manually.
<code><dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.1.Final</version>
</dependency></code>Two common request patterns:
POST/PUT with @RequestBody
GET with @RequestParam / @PathVariable
requestBody Parameter Validation
Define a DTO with constraint annotations and annotate the controller method argument with @Validated (or @Valid). Validation failures raise MethodArgumentNotValidException, which Spring translates to a 400 response.
<code>@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;
}</code> <code>@PostMapping("/save")
public Result saveUser(@RequestBody @Validated UserDTO userDTO) {
// business logic
return Result.ok();
}</code>requestParam / PathVariable Validation
For GET requests, place @Validated on the controller class and use constraint annotations on method parameters. Validation failures raise ConstraintViolationException.
<code>@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();
}
}</code>Unified Exception Handling
Define a @RestControllerAdvice that catches MethodArgumentNotValidException and ConstraintViolationException, builds a readable error message, and returns a custom Result with a business error code while keeping HTTP status 200.
<code>@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());
}
}</code>Advanced Usage
Group Validation
Define marker interfaces (e.g., Save, Update) and assign them to constraint annotations via the groups attribute. Apply @Validated(Group.class) on the controller method to trigger the appropriate group.
<code>@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;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 6, max = 20, groups = {Save.class, Update.class})
private String account;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 6, max = 20, groups = {Save.class, Update.class})
private String password;
}
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();
}</code>Nested Validation
When a DTO contains another object, annotate the nested field with @Valid so that its constraints are validated as well. Collections of objects can be validated by annotating the collection with @Valid.
<code>@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;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 6, max = 20, groups = {Save.class, Update.class})
private String account;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 6, max = 20, groups = {Save.class, Update.class})
private String password;
@NotNull(groups = {Save.class, Update.class})
@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;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String position;
}</code>Collection Validation
Wrap a List in a custom class annotated with @Valid to enable validation of each element in a JSON array.
<code>public class ValidationList<E> implements List<E> {
@Delegate
@Valid
private List<E> list = new ArrayList<>();
@Override
public String toString() {
return list.toString();
}
// other List methods delegated to 'list'
}</code> <code>@PostMapping("/saveList")
public Result saveList(@RequestBody @Validated(UserDTO.Save.class) ValidationList<UserDTO> userList) {
return Result.ok();
}</code>Custom Constraint
Create a new annotation annotated with @Constraint and implement ConstraintValidator to define custom validation logic, then use the annotation on fields.
<code>@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<? extends Payload>[] payload() default {};
}
public class EncryptIdValidator implements ConstraintValidator<EncryptId, String> {
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;
}
}</code>Programmatic Validation
Inject javax.validation.Validator and call validate(...) manually when you need explicit control.
<code>@Autowired
private javax.validation.Validator validator;
public Result saveWithCodingValidate(@RequestBody UserDTO userDTO) {
Set<ConstraintViolation<UserDTO>> violations = validator.validate(userDTO, UserDTO.Save.class);
if (!violations.isEmpty()) {
violations.forEach(v -> System.out.println(v));
}
return Result.ok();
}</code>Fail‑Fast Mode
Configure a Validator bean with failFast(true) so that validation stops at the first constraint violation.
<code>@Bean
public Validator validator() {
ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(true)
.buildValidatorFactory();
return factory.getValidator();
}</code>@Valid vs @Validated
Implementation Principles
requestBody Validation Mechanism
Spring MVC's RequestResponseBodyMethodProcessor resolves @RequestBody arguments, creates a WebDataBinder, and calls validateIfApplicable(). The binder ultimately delegates to Hibernate Validator via targetValidator.validate(...).
Method‑Level Validation Mechanism
MethodValidationPostProcessor registers an AOP advisor that applies MethodValidationInterceptor to beans annotated with @Validated. The interceptor uses ExecutableValidator to validate method parameters and return values, again delegating to Hibernate Validator.
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
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.