Mastering Spring Boot Validation: From Annotations to Programmatic Validators
This article explains how Spring Boot 3.4.2 supports both annotation‑based and programmatic validation, demonstrates when to choose each approach, provides step‑by‑step custom validator implementations for Employee and Department entities, and shows how to integrate these validators with Spring MVC controllers.
Introduction
Spring Boot 3.4.2 provides annotation‑based validation using constraints such as @NotNull and @Size, which automatically check data during binding or method calls, improving development efficiency and code robustness.
Annotation‑Based Validation
public class User {
@NotNull
private String username;
@Size(min = 6, max = 20)
private String password;
// ...
}By adding these annotations to entity fields, Spring’s validation framework triggers checks automatically.
Programmatic Validation
For more complex scenarios you can implement org.springframework.validation.Validator and write custom validation logic.
public class UserValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return User.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
User user = (User) target;
// custom validation logic
}
}When to Use Programmatic Validation
Use annotation validation for simple, declarative rules. Use programmatic validation when rules cannot be expressed with annotations, depend on dynamic conditions, or involve cross‑field interactions. In practice both can be combined to leverage their strengths.
Practical Example – Entities
public class Employee {
private Long id;
private String name;
private String email;
private Department department;
// getters, setters
}
public class Department {
private Long id;
private String name;
// getters, setters
}Custom Validators
public class EmployeeValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Employee.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmpty(errors, "id", "id.empty");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "姓名不能为空");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "email", "邮件不能为空");
Employee employee = (Employee) target;
if (employee.getName() != null && employee.getName().length() < 2) {
errors.rejectValue("name", "姓名必须大于等于2个字符");
}
}
}
public class DepartmentValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Department.class.equals(clazz);
}
@Override
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmpty(errors, "id", "id.empty");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "部门名称不能为空");
Department department = (Department) target;
if (department.getName() != null && department.getName().length() < 2) {
errors.rejectValue("name", "部门名称必须大于等于2个字符");
}
}
}Chaining Validators
When validating an Employee, you may also need to validate its associated Department. Use ValidationUtils.invokeValidator to delegate validation.
public class EmployeeValidator implements Validator {
private final DepartmentValidator departmentValidator;
public EmployeeValidator(DepartmentValidator departmentValidator) {
if (departmentValidator == null) {
throw new IllegalArgumentException("The supplied Validator is null.");
}
if (!departmentValidator.supports(Department.class)) {
throw new IllegalArgumentException("The supplied Validator must support the Department instances.");
}
this.departmentValidator = departmentValidator;
}
@Override
public boolean supports(Class<?> clazz) {
return Employee.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
// ... validate Employee fields ...
try {
errors.pushNestedPath("department");
ValidationUtils.invokeValidator(this.departmentValidator, ((Employee) target).getDepartment(), errors);
} finally {
errors.popNestedPath();
}
}
}Integration with Spring MVC Controller
@RestController
@RequestMapping("/employees")
public class EmployeeController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.setValidator(new EmployeeValidator(new DepartmentValidator()));
}
@PostMapping("/create")
public ResponseEntity<?> create(@Validated @RequestBody Employee employee, BindingResult error) {
if (error.hasErrors()) {
List<String> errors = error.getFieldErrors()
.stream()
.map(err -> err.getField() + ", " + err.getCode())
.toList();
return ResponseEntity.ok(errors);
}
return ResponseEntity.ok("success");
}
}The controller automatically applies the custom validators, returning a list of field‑error codes when validation fails.
Sample Test
Employee employee = new Employee();
EmployeeValidator employeeValidator = new EmployeeValidator(new DepartmentValidator());
Errors errors = new BeanPropertyBindingResult(employee, "employee");
employeeValidator.validate(employee, errors);
if (!errors.hasErrors()) {
System.out.println("Object is valid");
} else {
for (FieldError error : errors.getFieldErrors()) {
System.out.println(error.getField() + "," + error.getCode());
}
}Typical output:
id.empty
姓名不能为空
邮件不能为空Result of Chained Validation
id.empty
name,姓名不能为空
email,邮件不能为空
department.id,id.empty
department.name,部门名称不能为空Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
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.
