Master Spring Validation: Advanced Techniques, Group & Nested Validation, and Custom Rules

This comprehensive guide explores Spring Validation in depth, covering basic usage, dependency setup, requestBody and requestParam validation, unified exception handling, group validation, nested and collection validation, custom constraints, programmatic checks, fail‑fast mode, and the differences between @Valid and @Validated, while revealing the underlying implementation in Spring MVC.

Programmer DD
Programmer DD
Programmer DD
Master Spring Validation: Advanced Techniques, Group & Nested Validation, and Custom Rules

Simple Usage

The Java API (JSR‑303) defines the Bean Validation standard via validation-api, while hibernate-validator provides the implementation with annotations such as @Email and @Length. Spring Validation is a wrapper around Hibernate Validator that enables automatic validation of Spring MVC parameters. The article uses a spring-boot project to demonstrate Spring Validation.

Import Dependencies

If the Spring Boot version is lower than 2.3.x, spring-boot-starter-web automatically brings in hibernate-validator. For newer versions you must add the dependency manually:

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

Web services should validate parameters 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

For POST/PUT, a DTO object receives the request body. Adding @Validated on the DTO triggers automatic validation. Example: a UserDTO with constraints on userName, account, and password. Validation failures throw MethodArgumentNotValidException, which Spring converts to a 400 Bad Request.

DTO (Data Transfer Object) is used to receive request parameters as a Bean.

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 executes only after validation passes
    return Result.ok();
}

Both @Valid and @Validated can be used on method parameters.

requestParam/PathVariable Parameter Validation

For GET requests, annotate the Controller class with @Validated and place constraint annotations on method parameters, e.g., @Min. Validation failures throw ConstraintViolationException.

@RequestMapping("/api/user")
@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();
    }
}

Unified Exception Handling

When validation fails, either MethodArgumentNotValidException or ConstraintViolationException is thrown. A global @RestControllerAdvice 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

Different methods may require different validation rules for the same DTO. Define marker interfaces (e.g., Save, Update) and assign them to constraint annotations via the groups attribute. Then specify the group in @Validated on 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;
    }
}

Collection Validation

To validate each element of a collection, wrap the collection in a custom class and annotate the internal list with @Valid. Lombok's @Delegate can delegate list methods.

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 implement ConstraintValidator to define the validation logic.

@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 ctx) {
        return value == null || PATTERN.matcher(value).find();
    }
}

Programmatic Validation

Inject javax.validation.Validator and call validate manually for fine‑grained control.

@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 constraint 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

Spring

Group support

No

Yes

Target locations

METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE

TYPE, METHOD, PARAMETER

Nested validation

Supported

Not supported

Implementation Details

requestBody Validation Mechanism

Spring MVC uses RequestResponseBodyMethodProcessor. In resolveArgument() it reads the request body, creates a WebDataBinder, and calls validateIfApplicable(), which looks for @Validated or any annotation starting with "Valid" and then invokes binder.validate(). The binder delegates to Hibernate Validator.

Method‑Level Validation Mechanism

Spring registers an AOP advisor for beans annotated with @Validated via MethodValidationPostProcessor. The advisor uses MethodValidationInterceptor to intercept method calls, retrieve validation groups, and call ExecutableValidator.validateParameters() and validateReturnValue(). Both ultimately use Hibernate Validator.

Thus, whether validating request bodies or method parameters, Spring Validation is a thin wrapper around Hibernate Validator that provides convenient integration with Spring MVC and AOP.

Bean ValidationHibernate ValidatorGroup ValidationSpring ValidationCustom Constraint
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.