Master Spring Validation: Advanced Techniques, Group & Nested Validation in Spring Boot

This comprehensive guide dives deep into Spring Validation, covering basic usage, dependency setup, requestBody and requestParam validation, group and nested validation, custom constraints, fail‑fast mode, and the underlying implementation in Spring MVC and AOP, all with practical code examples.

Programmer DD
Programmer DD
Programmer DD
Master Spring Validation: Advanced Techniques, Group & Nested Validation in Spring Boot

Previously a brief article on Spring Validation was written; this tutorial provides a thorough exploration of Spring Validation, its best‑practice scenarios, and implementation principles, with source code available at the linked GitHub repository.

Simple Usage

The Java API (JSR‑303) defines the Bean Validation standard via validation-api, while Hibernate Validator implements it and adds annotations such as @Email and @Length. Spring Validation is a secondary wrapper of Hibernate Validator that enables automatic parameter validation for Spring MVC.

Import Dependency

If the Spring Boot version is lower than 2.3.x, spring-boot-starter-web automatically includes hibernate-validator. For versions higher than 2.3.x, the dependency must be added manually:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.1.Final</version>
</dependency>

For web services, parameter validation is essential in the Controller layer. Requests are typically of two forms:

POST/PUT requests use requestBody to pass parameters.

GET requests use requestParam or PathVariable to pass parameters.

requestBody Parameter Validation

POST/PUT requests usually receive a DTO object. Adding @Validated to the DTO enables automatic validation. Example: a UserDTO with constraints on userName (length 2‑10) and account / password (length 6‑20). Validation failures throw MethodArgumentNotValidException, which Spring converts to a 400 Bad Request.

DTO (Data Transfer Object) represents the bean used for request parameter binding in Spring Web.

Declare constraint annotations on DTO fields.

@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;
}
@PostMapping("/save")
public Result saveUser(@RequestBody @Validated UserDTO userDTO) {
    // business logic after successful validation
    return Result.ok();
}

Both @Valid and @Validated can be used on the method parameter.

requestParam/PathVariable Parameter Validation

GET requests use requestParam / PathVariable. For many parameters, a DTO is recommended; otherwise, annotate each method parameter. The Controller class must be annotated with @Validated, and parameters receive constraint annotations such as @Min.

@RequestMapping("/api/user")
@RestController
@Validated
public class UserController {
    @GetMapping("{userId}")
    public Result detail(@PathVariable("userId") @Min(10000000000000000L) Long userId) {
        // business logic after validation
        return Result.ok();
    }

    @GetMapping("getByAccount")
    public Result getByAccount(@Length(min = 6, max = 20) @NotNull String account) {
        // business logic after validation
        return Result.ok();
    }
}

Unified Exception Handling

When validation fails, MethodArgumentNotValidException or ConstraintViolationException is thrown. A global exception handler can convert these to a unified response with HTTP status 200 and a business error 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());
    }
}

Advanced Usage

Group Validation

When the same DTO is used in different scenarios with different validation rules, group validation can be applied. Define marker interfaces (e.g., Save, Update) and assign them to constraint annotations via the groups attribute. Then specify the group in @Validated at 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 with similar group settings
    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 to trigger validation of the inner object.

@Data
public class UserDTO {
    @NotNull @Valid private Job job;
    // other fields
    @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 collection, wrap the collection in a custom class and annotate the internal list with @Valid. This works with both group and nested validation.

public class ValidationList<E> implements List<E> {
    @Delegate
    @Valid
    public List<E> list = new ArrayList<>();
    @Override
    public String toString() { return list.toString(); }
}
@PostMapping("/saveList")
public Result saveList(@RequestBody @Validated(UserDTO.Save.class) ValidationList<UserDTO> userList) {
    return Result.ok();
}

Custom Validation

Create a custom constraint annotation (e.g., @EncryptId) and its validator implementing ConstraintValidator. The validator checks that the value matches a specific pattern.

@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).matches();
        }
        return true;
    }
}

Programmatic Validation

Inject javax.validation.Validator and call validate manually to obtain a set of constraint violations.

@Autowired
private javax.validation.Validator globalValidator;

@PostMapping("/saveWithCodingValidate")
public Result saveWithCodingValidate(@RequestBody UserDTO userDTO) {
    Set<ConstraintViolation<UserDTO>> 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 validation failure.

@Bean
public Validator validator() {
    ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
        .configure()
        .failFast(true)
        .buildValidatorFactory();
    return factory.getValidator();
}

@Valid vs @Validated

@Valid is defined by the JSR‑303 specification, does not support groups, and can be placed on methods, fields, constructors, parameters, and type use. @Validated is a Spring annotation, supports validation groups, and can be placed on types, methods, and parameters.

Implementation Principles

requestBody Validation

Spring MVC uses RequestResponseBodyMethodProcessor to read the request body, bind it to a DTO, and then invoke validateIfApplicable. This method checks for @Validated or any annotation starting with "Valid" and calls WebDataBinder.validate, which ultimately delegates to Hibernate Validator.

protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
    Annotation[] annotations = parameter.getParameterAnnotations();
    for (Annotation ann : annotations) {
        Validated validated = AnnotationUtils.getAnnotation(ann, Validated.class);
        if (validated != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
            Object hints = (validated != null ? validated.value() : AnnotationUtils.getValue(ann));
            Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
            binder.validate(validationHints);
            break;
        }
    }
}

Method‑Level Validation

Spring registers an AOP advisor for beans annotated with @Validated via MethodValidationPostProcessor. The advisor uses MethodValidationInterceptor to invoke Hibernate Validator on method parameters and return values.

public Object invoke(MethodInvocation invocation) throws Throwable {
    if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
        return invocation.proceed();
    }
    Class<?>[] groups = determineValidationGroups(invocation);
    ExecutableValidator exec = this.validator.forExecutables();
    Set<ConstraintViolation<Object>> result = exec.validateParameters(
        invocation.getThis(), invocation.getMethod(), invocation.getArguments(), groups);
    if (!result.isEmpty()) {
        throw new ConstraintViolationException(result);
    }
    Object returnValue = invocation.proceed();
    result = exec.validateReturnValue(invocation.getThis(), invocation.getMethod(), returnValue, groups);
    if (!result.isEmpty()) {
        throw new ConstraintViolationException(result);
    }
    return returnValue;
}

Both request‑body and method‑level validations ultimately rely on Hibernate Validator; Spring Validation merely provides a convenient wrapper.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaBackend DevelopmentSpring BootHibernate ValidatorSpring Validation
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

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.