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.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Mastering Spring Boot Validation: From Annotations to Programmatic Validators

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,部门名称不能为空
Diagram
Diagram
ValidationSpring BootProgrammatic Validator
Spring Full-Stack Practical Cases
Written by

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.

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.