Backend Development 17 min read

Comprehensive Guide to Spring Validation: Best Practices and Implementation Principles

This article provides an in‑depth tutorial on Spring Validation, covering simple usage, requestBody and requestParam/PathVariable validation, unified exception handling, advanced techniques such as group, nested, collection and custom validation, programmatic validation, fail‑fast mode, and the underlying implementation mechanisms within Spring MVC and Hibernate Validator.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Comprehensive Guide to Spring Validation: Best Practices and Implementation Principles

Simple Usage

The Java API specification (JSR‑303) defines a standard validation‑api but does not provide an implementation. Hibernate Validator implements this specification and adds annotations such as @Email and @Length . Spring Validation is a secondary wrapper around Hibernate Validator that enables automatic parameter validation in Spring MVC. For a Spring Boot project, add the dependency as shown below (Spring Boot < 2.3.x includes it automatically; for newer versions add it manually):

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

In a web service, controller parameters must be validated to prevent illegal input from affecting business logic. Requests are usually of two types: POST/PUT with requestBody and GET with requestParam/PathVariable .

1. requestBody Parameter Validation

For POST/PUT requests, use a DTO object annotated with @Validated . If validation fails, Spring throws MethodArgumentNotValidException , which is converted to a 400 Bad Request response.

@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 successful validation
    return Result.ok();
}

2. requestParam/PathVariable Parameter Validation

For GET requests, you can still use DTOs or place each parameter directly in the method signature. The controller class must be annotated with @Validated , and each parameter should have constraint annotations such as @Min . Validation failures raise ConstraintViolationException .

@RequestMapping("/api/user")
@RestController
@Validated
public class UserController {
    @GetMapping("{userId}")
    public Result detail(@PathVariable("userId") @Min(10000000000000000L) Long userId) {
        UserDTO userDTO = new UserDTO();
        userDTO.setUserId(userId);
        // ... set other fields
        return Result.ok(userDTO);
    }

    @GetMapping("getByAccount")
    public Result getByAccount(@Length(min = 6, max = 20) @NotNull String account) {
        UserDTO userDTO = new UserDTO();
        // ... set fields
        return Result.ok(userDTO);
    }
}

3. Unified Exception Handling

Both MethodArgumentNotValidException and ConstraintViolationException can be handled globally to return a friendly response, often 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

1. Group Validation

When the same DTO is used in different methods with different validation rules, define groups on constraint annotations and specify the group on @Validated .

@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 group definitions
    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();
}

2. Nested Validation

If a DTO field is another object, annotate the field with @Valid to trigger validation of the nested object.

@Data
public class UserDTO {
    // ... other fields
    @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;
        // ... other fields
    }
}

3. Collection Validation

To validate each element of a JSON array, wrap the collection in a custom class and annotate the collection field with @Valid .

public class ValidationList
implements List
{
    @Delegate // Lombok annotation
    @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();
}

4. Custom Validation

Create a custom constraint annotation and its validator to handle special rules, e.g., an encrypted ID consisting of 32‑256 hexadecimal characters.

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EncryptIdValidator.class})
public @interface EncryptId {
    String message() default "Invalid encrypted ID format";
    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;
    }
}

5. Programmatic Validation

Inject javax.validation.Validator and call its API directly when annotation‑based validation is not suitable.

@Autowired
private javax.validation.Validator globalValidator;

@PostMapping("/saveWithCodingValidate")
public Result saveWithCodingValidate(@RequestBody UserDTO userDTO) {
    Set
> validate = globalValidator.validate(userDTO, UserDTO.Save.class);
    if (!validate.isEmpty()) {
        for (ConstraintViolation
v : validate) {
            System.out.println(v);
        }
    }
    return Result.ok();
}

6. Fail‑Fast Mode

Configure Hibernate Validator to stop after the first constraint violation.

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

7. Difference Between @Valid and @Validated

Both trigger validation, but @Validated supports validation groups while @Valid does not. (See the accompanying image in the original article.)

Implementation Principles

1. requestBody Validation Mechanism

Spring MVC uses RequestResponseBodyMethodProcessor to resolve @RequestBody arguments. Inside resolveArgument() , after reading the request body, it calls validateIfApplicable() , which checks for @Validated or any annotation whose name starts with "Valid" and then invokes binder.validate() . The binder ultimately delegates to Hibernate Validator.

2. Method‑Level Validation Mechanism

Method‑level validation is implemented via AOP. MethodValidationPostProcessor registers an advisor for beans annotated with @Validated . The advisor uses MethodValidationInterceptor , which calls ExecutableValidator.validateParameters() before method execution and validateReturnValue() after execution, both delegating to Hibernate Validator.

Project source code: https://github.com/chentianming11/spring-validation

Final Note

The author invites readers to follow the public account “码猿技术专栏” to obtain PDF collections of Spring Cloud, Spring Boot, and MyBatis advanced tutorials, and encourages likes, shares, and comments to support future content creation.

backendJavaSpringvalidationSpring BootHibernate Validator
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

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.