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.
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 @RequestParamor 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
