Spring Boot Parameter Validation: From Basic Annotations to Custom Validators & Group Validation

This guide covers Spring Boot parameter validation using Bean Validation annotations, demonstrating basic setup with @Valid/@Validated, custom validator creation for phone numbers, group validation for create/update scenarios, global exception handling for MethodArgumentNotValidException and ConstraintViolationException, and best practices like DTO separation and nested object validation.

Xiaolin Talks Programming
Xiaolin Talks Programming
Xiaolin Talks Programming
Spring Boot Parameter Validation: From Basic Annotations to Custom Validators & Group Validation

Why Parameter Validation Matters

In early development, defensive validation code often clutters service layers. The article shows a typical createUser method littered with if (StringUtils.isEmpty(dto.getName())) checks for name, age, and email format. This approach has three drawbacks: bloated code mixing validation with business logic, difficult maintenance when rules change, and poor reusability since the same DTO may need different rules across endpoints.

Spring Boot integrates Hibernate Validator (the Bean Validation reference implementation) to provide declarative validation.

Quick Start

1. Add Dependency

Before Spring Boot 2.3.x, spring-boot-starter-web included validation transitively. From 2.3.x onward, add explicitly:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

2. Annotate the DTO

@Data
public class UserDTO {
    @NotBlank(message = "用户名不能为空")
    @Size(min = 2, max = 20, message = "用户名长度必须在 2-20 之间")
    private String username;

    @NotNull(message = "年龄不能为空")
    @Min(value = 0, message = "年龄必须大于等于 0")
    @Max(value = 150, message = "年龄必须小于等于 150")
    private Integer age;

    @Email(message = "邮箱格式不正确")
    private String email;
}

3. Enable Validation in Controller

Add @Valid or @Validated on the @RequestBody parameter:

@RestController
@RequestMapping("/user")
public class UserController {
    @PostMapping
    public Result<Void> createUser(@Valid @RequestBody UserDTO userDTO) {
        // Validation failure throws MethodArgumentNotValidException before reaching here
        userService.createUser(userDTO);
        return Result.success();
    }
}

Advanced Usage

@Valid vs @Validated

@Valid (JSR-303 standard): supports nested validation — if a field is another annotated object, its constraints are also validated.

@Validated (Spring extension): more powerful, supports group validation , but does not trigger nested validation by default (must combine with @Valid on the nested field).

Typical pattern: use @Validated on controller parameters, @Valid on nested DTO fields.

Custom Validation Annotation (Phone Number Example)

Step 1: Define Annotation

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
    String message() default "手机号格式不正确";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Step 2: Implement Validator

public class PhoneValidator implements ConstraintValidator<Phone, String> {
    private static final String PHONE_REGEX = "^1[3-9]\\d{9}$";

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (StringUtils.isEmpty(value)) {
            return true; // emptiness handled by @NotBlank/@NotNull
        }
        return value.matches(PHONE_REGEX);
    }
}

Step 3: Use It

@Data
public class UserDTO {
    // ... other fields
    @Phone(message = "请输入有效的手机号码")
    private String phone;
}

Group Validation (Validation Groups)

Same DTO, different rules for create vs update. Example: id must be null on create, non-null on update.

Define Group Interfaces

public interface CreateGroup {}
public interface UpdateGroup {}

Configure DTO with Groups

@Data
public class UserDTO {
    @Null(message = "创建用户时 ID 必须为空", groups = CreateGroup.class)
    @NotNull(message = "更新用户时 ID 不能为空", groups = UpdateGroup.class)
    private Long id;

    @NotBlank(message = "用户名不能为空", groups = {CreateGroup.class, UpdateGroup.class})
    private String username;
}

Specify Group in Controller

@PostMapping
public Result<Void> createUser(@Validated(CreateGroup.class) @RequestBody UserDTO userDTO) { ... }

@PutMapping
public Result<Void> updateUser(@Validated(UpdateGroup.class) @RequestBody UserDTO userDTO) { ... }
Note: Fields without an explicit groups attribute belong to the Default group. Once a field declares a custom group, it no longer belongs to Default . If the controller uses plain @Validated (Default group), those custom-group constraints will be skipped.

Global Exception Handling

Validation failures throw different exceptions: @RequestBody

MethodArgumentNotValidException
@RequestParam

or path variables → ConstraintViolationException Handle both in a @RestControllerAdvice:

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<?> handleValidationException(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getFieldErrors().stream()
                .map(DefaultMessageSourceResolvable::getDefaultMessage)
                .collect(Collectors.joining(", "));
        return Result.fail(400, message);
    }

    @ExceptionHandler(ConstraintViolationException.class)
    public Result<?> handleConstraintViolation(ConstraintViolationException e) {
        String message = e.getConstraintViolations().stream()
                .map(ConstraintViolation::getMessage)
                .collect(Collectors.joining(", "));
        return Result.fail(400, message);
    }
}

Best Practices Summary

Separate DTO from Entity : Put validation annotations on DTOs, not on database entities.

Fail-Fast : Default validator collects all errors. For fail-fast, use @GroupSequence or a custom Validator implementation.

Explicit Error Messages : Every constraint should declare a clear message for direct frontend display.

Nested Object Validation : If a DTO contains another object (e.g., AddressDTO address), annotate the field with @Valid to trigger inner validation.

Cross-Field Validation : For rules spanning multiple fields (e.g., password == confirmPassword), create a class-level custom constraint annotation.

Conclusion

Parameter validation is the first line of defense. Spring Validation transforms scattered if-else checks into declarative annotations, and combined with global exception handling, yields an elegant, maintainable validation layer. Mastering these techniques makes API development twice as effective.

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.

backend-developmentexception handlingbean-validationSpring BootHibernate Validatorparameter-validationcustom-validatorgroup-validation
Xiaolin Talks Programming
Written by

Xiaolin Talks Programming

Focuses on sharing original technical insights. Senior architect at a top tech company with years of experience in technical architecture and management, and extensive interview experience. Offers one-on-one technical coaching, guiding you from beginner to architecture design to technical management. Follow for free learning resources. Free one-on-one interview coaching to help you land offers quickly.

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.