Backend Development 17 min read

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.

macrozheng
macrozheng
macrozheng
Master Spring Validation: From Simple Use to Advanced Techniques and Internals

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>&lt;dependency&gt;
    &lt;groupId&gt;org.hibernate&lt;/groupId&gt;
    &lt;artifactId&gt;hibernate-validator&lt;/artifactId&gt;
    &lt;version&gt;6.0.1.Final&lt;/version&gt;
&lt;/dependency&gt;</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

Valid vs Validated comparison
Valid vs Validated comparison

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.

JavaSpringValidationSpring BootHibernate Validator
macrozheng
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.