Spring Boot
JSR-303
Custom Validation
Spring Validator
Endpoint Validation

Implementing custom validation logic for a Spring Boot endpoint using a combination of JSR-303 and Spring's Validator

Data Structures & Algorithms practice on Codemia

Step through 300+ coding interview problems with animated visualisers that show the data structure changing as the code runs. The Blind 75 are free.

Practice algorithms

Introduction

In Spring Boot, bean validation annotations handle a lot of field-level rules well, but they are not enough for every endpoint. Cross-field checks, conditional rules, and domain-specific validation often need extra logic. A good pattern is to keep simple declarative rules in JSR-303 annotations and layer more complex endpoint validation through Spring's Validator interface.

Use JSR-303 for Simple Structural Rules

Start by putting basic field constraints directly on the request DTO.

java
1import jakarta.validation.constraints.Email;
2import jakarta.validation.constraints.NotBlank;
3import jakarta.validation.constraints.Size;
4
5public class UserRequest {
6    @NotBlank
7    private String username;
8
9    @Email
10    private String email;
11
12    @Size(min = 8)
13    private String password;
14
15    private String confirmPassword;
16
17    public String getUsername() { return username; }
18    public void setUsername(String username) { this.username = username; }
19    public String getEmail() { return email; }
20    public void setEmail(String email) { this.email = email; }
21    public String getPassword() { return password; }
22    public void setPassword(String password) { this.password = password; }
23    public String getConfirmPassword() { return confirmPassword; }
24    public void setConfirmPassword(String confirmPassword) { this.confirmPassword = confirmPassword; }
25}

This keeps obvious shape validation close to the data model.

Add Spring Validator for Custom Logic

Now add rules that do not fit cleanly into a single annotation, such as matching passwords or business-specific conditions.

java
1import org.springframework.stereotype.Component;
2import org.springframework.validation.Errors;
3import org.springframework.validation.Validator;
4
5@Component
6public class UserRequestValidator implements Validator {
7
8    @Override
9    public boolean supports(Class<?> clazz) {
10        return UserRequest.class.isAssignableFrom(clazz);
11    }
12
13    @Override
14    public void validate(Object target, Errors errors) {
15        UserRequest request = (UserRequest) target;
16
17        if (request.getPassword() != null &&
18            !request.getPassword().equals(request.getConfirmPassword())) {
19            errors.rejectValue("confirmPassword", "password.mismatch", "Passwords do not match");
20        }
21
22        if (request.getUsername() != null && request.getUsername().contains("admin")) {
23            errors.rejectValue("username", "username.reserved", "Username contains a reserved term");
24        }
25    }
26}

This is where you place logic that depends on several fields or on domain-specific policy.

Wire It into the Endpoint

A controller can run both styles of validation by using @Valid and then invoking the custom validator.

java
1import org.springframework.http.ResponseEntity;
2import org.springframework.validation.BindingResult;
3import org.springframework.web.bind.annotation.PostMapping;
4import org.springframework.web.bind.annotation.RequestBody;
5import org.springframework.web.bind.annotation.RestController;
6import jakarta.validation.Valid;
7
8@RestController
9public class UserController {
10
11    private final UserRequestValidator userRequestValidator;
12
13    public UserController(UserRequestValidator userRequestValidator) {
14        this.userRequestValidator = userRequestValidator;
15    }
16
17    @PostMapping("/users")
18    public ResponseEntity<?> createUser(@Valid @RequestBody UserRequest request, BindingResult bindingResult) {
19        userRequestValidator.validate(request, bindingResult);
20
21        if (bindingResult.hasErrors()) {
22            return ResponseEntity.badRequest().body(bindingResult.getAllErrors());
23        }
24
25        return ResponseEntity.ok("created");
26    }
27}

The sequence matters:

  • annotation-based validation runs
  • custom validator adds extra errors
  • the controller decides whether to continue

Why This Split Is Useful

Separating the validation styles keeps the code easier to reason about.

Use annotations for:

  • null checks
  • length constraints
  • format checks
  • simple field requirements

Use a Spring validator for:

  • cross-field consistency
  • conditional rules
  • repository-backed checks
  • business-specific rules that are awkward as annotations

That division usually produces cleaner DTOs and more maintainable validation code.

Alternative: Custom Bean Validation Annotation

If a rule is reused across many DTOs, a custom bean validation annotation may be better than a Spring Validator. But for endpoint-specific logic, the Spring validator approach is often simpler and more direct.

In other words:

  • reusable domain rule across many places: consider a custom constraint annotation
  • endpoint-specific orchestration rule: Spring validator is often enough

Common Pitfalls

  • Stuffing every rule into custom validators and losing the simplicity of built-in annotations.
  • Putting complex cross-field logic into one field annotation where the intent becomes hard to read.
  • Forgetting to inspect BindingResult after running the custom validator.
  • Doing expensive database lookups in validation without being clear about the performance cost.
  • Mixing validation and business side effects. Validation should report problems, not create or change state.

Summary

  • Use JSR-303 annotations for straightforward field-level validation.
  • Use Spring's Validator for cross-field and endpoint-specific rules.
  • Call the custom validator after @Valid so both validation layers contribute to the same BindingResult.
  • Keep validation declarative where possible and custom where necessary.
  • The best combination is usually not all annotations or all custom code, but a clear split between simple and domain-specific rules.

Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Data Structures & Algorithms practice on Codemia

Step through 300+ coding interview problems with animated visualisers that show the data structure changing as the code runs. The Blind 75 are free.

Practice algorithms