Backend Development 16 min read

Comprehensive Guide to Spring Validation: Best Practices, Scenarios, and Advanced Features

This article provides an in‑depth tutorial on Spring Validation, covering basic usage, dependency configuration, requestBody and requestParam validation, unified exception handling, group and nested validation, collection checks, custom constraints, programmatic validation, fail‑fast mode, and the differences between @Valid and @Validated.

Architecture Digest
Architecture Digest
Architecture Digest
Comprehensive Guide to Spring Validation: Best Practices, Scenarios, and Advanced Features

Spring Validation is a powerful mechanism built on the JSR‑303 Bean Validation API (validation‑api) and implemented by Hibernate Validator. It adds annotations such as @Email and @Length and integrates with Spring MVC to automatically validate controller method parameters.

Simple Usage

The spring-boot-starter-web starter pulls in hibernate-validator automatically for Spring Boot versions below 2.3.x. For newer versions you must add the dependency manually:

org.hibernate
hibernate-validator
6.0.1.Final

For web services, validation should be performed in the Controller layer. POST/PUT requests use a request body, while GET requests use @RequestParam or @PathVariable .

RequestBody Parameter Validation

Define a DTO and annotate it with @Validated (or @Valid ) on the controller method:

@PostMapping("/save")
public Result saveUser(@RequestBody @Validated UserDTO userDTO) {
    // business logic executes only after successful validation
    return Result.ok();
}

If validation fails, Spring throws MethodArgumentNotValidException and returns a 400 Bad Request response by default.

RequestParam / PathVariable Validation

Annotate the controller class with @Validated and place constraint annotations on the method parameters:

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

Example of validating a path variable:

@GetMapping("{userId}")
public Result detail(@PathVariable("userId") @Min(10000000000000000L) Long userId) {
    // logic after successful validation
    UserDTO userDTO = new UserDTO();
    userDTO.setUserId(userId);
    return Result.ok(userDTO);
}

Advanced Usage

Group Validation

Define validation groups to apply different rules for save and update operations:

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

    public interface Save {}
    public interface Update {}
}

Apply the group in the controller:

@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

When a DTO contains another object, annotate the nested field with @Valid to trigger cascade validation:

@Data
public class UserDTO {
    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 2, max = 10, groups = {Save.class, Update.class})
    private String userName;

    @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;
    }
}

Collection Validation

Wrap a collection in a custom class and annotate the list with @Valid to validate each element:

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

Custom Constraint

Create a custom annotation and validator to check encrypted IDs (32‑256 characters, digits or a‑f):

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

Programmatic Validation

Inject javax.validation.Validator and invoke it manually:

@Autowired
private javax.validation.Validator globalValidator;

@PostMapping("/saveWithCodingValidate")
public Result saveWithCodingValidate(@RequestBody UserDTO userDTO) {
    Set
> 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 at the first 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 (Bean Validation)

Spring

Group support

No

Yes

Target locations

METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE

TYPE, METHOD, PARAMETER

Nested validation

Supported

Not supported

Implementation Principles

Spring MVC’s RequestResponseBodyMethodProcessor resolves method arguments, creates a WebDataBinder , and calls validateIfApplicable . This method scans for @Validated or any annotation whose name starts with “Valid” and triggers binder.validate() , which delegates to Hibernate Validator.

Method‑level validation is implemented via AOP. MethodValidationPostProcessor registers an advisor for beans annotated with @Validated . The advisor uses MethodValidationInterceptor , which invokes ExecutableValidator.validateParameters and validateReturnValue from Hibernate Validator, throwing ConstraintViolationException on failure.

Thus, both request‑body validation and method‑level validation ultimately rely on Hibernate Validator; Spring merely provides the integration layer.

backendjavaDTOSpringvalidationannotationsHibernate Validator
Architecture Digest
Written by

Architecture Digest

Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.

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.