Picture this: a user signs up on your site. They submit a form. Somewhere on the way to the database, a field is missing, or a password is two characters long, or someone typed an email address that isn't really an email. If you don't catch it, that bad data lands in your database and lives there forever.
You could write a wall of if (user.getEmail() == null) { ... } checks at the top of every controller method. Spring offers a much better path: Spring Validation, a thin layer on top of the Jakarta Bean Validation spec that lets you describe your rules once, on the entity, and have them enforced everywhere the entity is read in.
The annotation toolbox
Validation rules attach to fields. The handful you'll reach for constantly:
| Annotation | What it enforces |
|---|---|
@NotNull | Field must not be null. Says nothing about empty strings. |
@NotEmpty | For collections and strings — must not be null and must have length > 0. |
@NotBlank | For strings — must not be null and must contain at least one non-whitespace character. |
@Size(min, max) | Length bounds for strings, collections, arrays. |
@Min(n) / @Max(n) | Numeric bounds. |
@Email | Must look like an email address. |
@Pattern(regexp) | Must match a regex. |
Each one accepts a message attribute that's returned to the client when the rule fails:
1@NotBlank(message = "Username cannot be empty")
2private String username;
Setting it up
You need the validation starter on your classpath. With Spring Boot:
pom.xml1<dependency>
2 <groupId>org.springframework.boot</groupId>
3 <artifactId>spring-boot-starter-validation</artifactId>
4</dependency>
That pulls in Hibernate Validator, the reference implementation of Jakarta Bean Validation.
Wiring up an entity
Decorate the fields you care about:
src/main/java/com/example/dto/SignupRequest.java 1public class SignupRequest {
2
3 @NotBlank(message = "Username is required")
4 @Size(min = 3, max = 20, message = "Username must be 3–20 characters")
5 private String username;
6
7 @NotBlank
8 @Email(message = "Must be a valid email address")
9 private String email;
10
11 @NotBlank
12 @Size(min = 8, message = "Password must be at least 8 characters")
13 @Pattern(
14 regexp = "^(?=.*[A-Z])(?=.*\\d).+$",
15 message = "Password must contain at least one uppercase letter and one digit"
16 )
17 private String password;
18
19 @Min(value = 18, message = "You must be 18 or older")
20 private int age;
21
22 // getters and setters
23}
The class doesn't do validation yet — it just describes the rules.
Triggering validation in a controller
The magic word is @Valid. Put it in front of the @RequestBody parameter and Spring runs every annotation before your handler method gets called:
src/main/java/com/example/web/SignupController.java 1@RestController
2@RequestMapping("/api/signup")
3public class SignupController {
4
5 @PostMapping
6 public ResponseEntity<String> signup(@Valid @RequestBody SignupRequest request) {
7 // If we get here, every rule has passed.
8 // Save the user, return 201, etc.
9 return ResponseEntity.status(HttpStatus.CREATED).body("Welcome!");
10 }
11}
If any rule fails, Spring throws a MethodArgumentNotValidException and the response goes out as HTTP 400 Bad Request without your handler ever running.
Handling the failure
Spring's default 400 response is fine for development, but production usually wants something more structured. A @RestControllerAdvice lets you shape the response:
1@RestControllerAdvice
2public class ValidationExceptionHandler {
3
4 @ExceptionHandler(MethodArgumentNotValidException.class)
5 public ResponseEntity<Map<String, String>> handleValidation(
6 MethodArgumentNotValidException ex
7 ) {
8 Map<String, String> errors = new HashMap<>();
9 ex.getBindingResult().getFieldErrors().forEach(error ->
10 errors.put(error.getField(), error.getDefaultMessage())
11 );
12 return ResponseEntity.badRequest().body(errors);
13 }
14}
Now a request that misses three fields gets a friendly JSON body back:
1{
2 "username": "Username is required",
3 "email": "Must be a valid email address",
4 "password": "Password must be at least 8 characters"
5}
Custom rules
When the built-in annotations don't cover what you need, you can write your own. The recipe is:
- Define an annotation.
- Define a
ConstraintValidatorimplementation. - Use it like any other validation annotation.
src/main/java/com/example/validation/StartsWithLetter.java1@Target({ ElementType.FIELD, ElementType.PARAMETER })
2@Retention(RetentionPolicy.RUNTIME)
3@Constraint(validatedBy = StartsWithLetterValidator.class)
4public @interface StartsWithLetter {
5 String message() default "Must start with a letter";
6 Class<?>[] groups() default {};
7 Class<? extends Payload>[] payload() default {};
8}
1public class StartsWithLetterValidator
2 implements ConstraintValidator<StartsWithLetter, String> {
3
4 @Override
5 public boolean isValid(String value, ConstraintValidatorContext ctx) {
6 return value != null && !value.isEmpty() && Character.isLetter(value.charAt(0));
7 }
8}
1@StartsWithLetter
2private String username;
Wrap-up
Spring Validation is one of those features that pays for itself quickly:
- The rules live next to the fields they describe, so they're easy to find and easy to change.
- Controllers stay focused on business logic, not defensive
if-checks. - Bad requests get rejected with a 400 before your handler runs, which means less work for the rest of your stack.
The full payoff comes when you combine it with a global exception handler — your API surface ends up with consistent, well-shaped error responses across every endpoint, without each controller having to think about it.
