Bean Validation
Reviewed & published by Brayan K
Bean Validation lets you declare input rules with annotations — @NotNull, @Size, @Email — and have Spring enforce them automatically with @Valid, surfacing problems through a BindingResult or a 400 response.
Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
Before You Start
You should know how a Spring REST controller binds a @RequestBody. These examples use the jakarta.validation constraints.
What You'll Learn
- Declaring constraints with jakarta.validation
- @NotNull vs @NotEmpty vs @NotBlank
- @Size, @Email, @Min, @Max
- Triggering validation with @Valid
- Inspecting errors with BindingResult
- Customizing constraint messages
The Big Idea — A Bouncer at the Door
💡 Analogy: Validation annotations are the guest list rules posted at the door, and @Valid is the bouncer who actually checks them. The rules — "name required, 2-30 characters," "must be 18+" — live on the request object. When a request arrives, the bouncer reads each rule; if anything fails, the visitor is turned away (a 400) or you get a clipboard of every problem (the BindingResult) to handle yourself. Bad data never reaches the party.
Declare the rules once; let the framework enforce them everywhere.
1️⃣ Declaring Constraints
Annotate the fields of a request object. @NotBlank for required text, @Size for length, @Email for format, @Min/@Max for ranges. Each takes an optional message.
import jakarta.validation.constraints.*;
// Constraints declared right on the fields of a request object.
public class CreateUserRequest {
@NotBlank(message = "name is required")
@Size(min = 2, max = 30, message = "name must be 2-30 characters")
private String name;
@NotNull
@Email(message = "must be a valid email address")
private String email;
@Min(value = 18, message = "must be at least 18")
@Max(value = 120, message = "age looks invalid")
private int age;
// getters/setters omitted for brevity
public String getName() { return name; }
public String getEmail() { return email; }
public int getAge() { return age; }
}2️⃣ Enforcing with @Valid
Put @Valid on the @RequestBody parameter. With no BindingResult, a failure becomes a 400 Bad Request automatically; add a BindingResult right after it to inspect errors yourself.
import jakarta.validation.Valid;
import org.springframework.http.*;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/users")
public class UserController {
// Option A: let @Valid raise a 400 automatically on failure.
@PostMapping
public ResponseEntity<String> create(@Valid @RequestBody CreateUserRequest req) {
// Reached only when the request is valid.
return ResponseEntity.ok("created " + req.getName());
}
// Option B: add a BindingResult to inspect errors yourself.
@PostMapping("/manual")
public ResponseEntity<?> createManual(@Valid @RequestBody CreateUserRequest req,
BindingResult result) {
if (result.hasErrors()) {
String first = result.getFieldErrors().get(0).getDefaultMessage();
return ResponseEntity.badRequest().body("invalid: " + first);
}
return ResponseEntity.ok("created " + req.getName());
}
}3️⃣ What the Constraints Actually Do
Under the hood, a validator runs rules like the ones below and collects every failure. Here is the same logic in plain Java so you can run it.
import java.util.*;
// PLAIN-JAVA illustration of what @NotBlank / @Size / @Min DO.
// Bean Validation runs rules like these and collects the failures.
public class Main {
record CreateUser(String name, String email, int age) { }
static List<String> validate(CreateUser u) {
List<String> errors = new ArrayList<>();
// @NotBlank
if (u.name() == null || u.name().trim().isEmpty())
errors.add("name is required");
// @Size(min=2, max=30)
else if (u.name().length() < 2 || u.name().length() > 30)
errors.add("name must be 2-30 characters");
// @Email (very simplified)
if (u.email() == null || !u.email().contains("@"))
errors.add("must be a valid email address");
// @Min(18)
if (u.age() < 18) errors.add("must be at least 18");
return errors;
}
public static void main(String[] args) {
CreateUser bad = new CreateUser(" ", "not-an-email", 16);
List<String> errors = validate(bad);
System.out.println("valid? " + errors.isEmpty());
errors.forEach(e -> System.out.println(" - " + e));
CreateUser good = new CreateUser("Ana", "ana@site.com", 31);
System.out.println("valid? " + validate(good).isEmpty());
}
}🧠 Quick Recall
Predict before you peek.
Answer: it passes @NotNull (it isn't null) but fails @NotBlank (no non-whitespace characters).
Answer: 400 Bad Request, via MethodArgumentNotValidException.
Answer: immediately after the @Valid parameter it reports on.
🎯 YOUR TURN — Validate a ProductRequest
Annotate a ProductRequest with constraints for name, sku, price, and an optional description.
import jakarta.validation.constraints.*;
// YOUR TURN: annotate a ProductRequest.
// - name: not blank, 1-100 characters
// - sku: not null, exactly... use @Size(min=8, max=8)
// - priceCents: @Min(1) (must cost something)
// - description: optional, but if present @Size(max=500)
//
// public class ProductRequest {
// @NotBlank @Size(min = 1, max = 100) private String name;
// ...
// }🧩 MINI-CHALLENGE — Return all field errors
Build a controller that collects every validation error into a Map<String,String> and returns it as a 400.
import jakarta.validation.Valid;
import org.springframework.http.*;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import java.util.*;
// MINI-CHALLENGE: a controller that returns ALL field errors as a map.
// - @PostMapping that takes @Valid @RequestBody ProductRequest + BindingResult
// - if result.hasErrors(): build Map<String,String> of field -> message
// from result.getFieldErrors() and return 400 with that body
// - otherwise return 200 with the created product
//
// Think: why collect every error instead of just the first?Common Errors
- ❌ Forgetting @Valid. The annotations are ignored. Fix: add @Valid to the parameter.
- ❌ Using @NotNull for required text. Empty strings sneak through. Fix: use @NotBlank.
- ❌ Missing the validation starter. No validator on the classpath. Fix: add spring-boot-starter-validation.
- ❌ Wrong import (javax). Modern Spring uses Jakarta. Fix: import jakarta.validation.constraints.
- ❌ BindingResult in the wrong position. Errors aren't bound. Fix: place it right after the @Valid argument.
- ❌ Trusting @Email as proof. It only checks format. Fix: add a real verification step for important emails.
Pro Tips
- 💡 Validate DTOs, not entities — keep request rules at the edge of your app.
- 💡 Give every constraint a clear message so clients know how to fix the input.
- 💡 Collect all errors (iterate getFieldErrors()) rather than returning only the first.
- 💡 Combine constraints — e.g. @NotBlank @Size(max = 30) — for both presence and bounds.
📋 Quick Reference
| Annotation | Checks | Note |
|---|---|---|
| @NotNull | not null | Empty string OK |
| @NotEmpty | not null + non-empty | String/collection |
| @NotBlank | non-whitespace text | Strings only |
| @Size(min,max) | length/size range | String/collection |
| email format | Format only | |
| @Min / @Max | numeric bounds | Inclusive |
| @Valid | triggers validation | On the parameter |
| BindingResult | holds errors | After @Valid arg |
| message = "..." | custom error text | Per constraint |
| default failure | 400 Bad Request | No BindingResult |
📚 Resources / Keep Learning
- 📘 Jakarta Bean Validation 3.0 specification (jakarta.ee)
- 📗 Spring Framework — Bean Validation (docs.spring.io)
- 📗 Hibernate Validator documentation (hibernate.org)
🎉 Lesson Complete!
You can now declare input rules with jakarta.validation constraints, choose between @NotNull, @NotEmpty, and @NotBlank, enforce them with @Valid, and inspect failures through a BindingResult or a default 400.
Practice quiz
Which package do the standard Bean Validation annotations live in (modern Spring Boot)?
- javax.validation
- jakarta.validation.constraints
- org.springframework.validation
- java.validation
Answer: jakarta.validation.constraints. Modern Spring Boot uses Jakarta Bean Validation; constraints live in jakarta.validation.constraints.
What does @NotNull check?
- The value is not null
- The String is not blank
- The collection is not empty
- The number is positive
Answer: The value is not null. @NotNull simply requires the annotated value to be non-null (it allows an empty string).
Which annotation rejects null, empty, AND whitespace-only strings?
- @NotNull
- @NotEmpty
- @NotBlank
- @Size(min=1)
Answer: @NotBlank. @NotBlank requires a non-null string with at least one non-whitespace character.
What does @Size(min = 2, max = 30) constrain?
- Only numbers
- The byte size of a file
- The number of fields
- The length/size of a string, collection, or array
Answer: The length/size of a string, collection, or array. @Size constrains the length of a String or the size of a collection/array/map to the given range.
What triggers validation of a @RequestBody in a controller?
- @Validated on the class only
- Annotating the parameter with @Valid
- It is always automatic
- A try/catch block
Answer: Annotating the parameter with @Valid. Placing @Valid (or @Validated) on the method parameter tells Spring to validate the bound object.
Where do you inspect validation errors when you add a BindingResult parameter?
- In the response headers
- In the BindingResult immediately after the @Valid parameter
- In a static field
- In application.properties
Answer: In the BindingResult immediately after the @Valid parameter. A BindingResult declared right after the validated parameter holds the field errors for manual handling.
What does @Email validate?
- The string is a syntactically valid email address
- The address exists
- The domain is reachable
- The inbox is empty
Answer: The string is a syntactically valid email address. @Email only checks that the string matches an email-address format; it cannot confirm deliverability.
Which annotation requires a numeric value to be at least a minimum?
- @Max
- @Min
- @Positive
- @Size
Answer: @Min. @Min(value) requires the number to be greater than or equal to the given minimum.
By default, what HTTP status does Spring return when @Valid fails on a @RequestBody (no BindingResult)?
- 200 OK
- 500 Internal Server Error
- 400 Bad Request
- 404 Not Found
Answer: 400 Bad Request. A failed @Valid on a request body raises MethodArgumentNotValidException, mapped to 400 Bad Request by default.
What is the role of the message attribute on a constraint, e.g. @Size(message = "...")?
- It changes the field name
- It disables the constraint
- It sets a default value
- It customizes the validation error message
Answer: It customizes the validation error message. The message attribute customizes the error text reported when the constraint is violated.
Continue this course
- Previous: Derived Queries & @Query
- Next: Exception Handling with @ControllerAdvice — @ControllerAdvice, @ExceptionHandler and clean error responses
- Quick reference: Java cheat sheet
Frequently asked questions
What is Bean Validation and where do the annotations come from?
Bean Validation (the Jakarta Validation specification) is a standard way to declare constraints on object fields using annotations like @NotNull, @Size, @Email, and @Min, then have a validator check them. In modern Spring Boot the annotations live in jakarta.validation.constraints, and the spring-boot-starter-validation dependency brings in an implementation (Hibernate Validator). You annotate the fields once and the framework enforces them, rather than scattering if-checks throughout your code.
What is the difference between @NotNull, @NotEmpty, and @NotBlank?
@NotNull only rejects a null value — an empty string "" passes. @NotEmpty rejects null and also empty: for a string it requires length greater than zero, and for a collection it requires at least one element, but a string of spaces " " still passes. @NotBlank applies only to strings and is the strictest: it rejects null, empty, and whitespace-only values, requiring at least one non-whitespace character. For required text fields, @NotBlank is usually what you want.
How does @Valid actually trigger validation in a controller?
When you annotate a controller method parameter — typically a @RequestBody — with @Valid (or Spring's @Validated), Spring runs the bean validator on the bound object after deserialization. If a constraint fails and there is no BindingResult parameter, Spring throws MethodArgumentNotValidException, which by default is translated into a 400 Bad Request response with the error details. Without @Valid, the annotations on the object are present but never checked for that request.
When should I use a BindingResult parameter?
Add a BindingResult immediately after the @Valid parameter when you want to handle validation failures yourself instead of letting Spring throw. With a BindingResult you can call hasErrors(), iterate getFieldErrors(), build a custom error response, log, or apply fallback logic. Without it, validation failures short-circuit into an exception and the default error handling. The order matters: the BindingResult must directly follow the object it reports on.
Can @Email guarantee the address is real?
No. @Email only checks that the string is syntactically a plausible email address — it matches the expected format. It cannot verify that the mailbox exists, that the domain is reachable, or that mail would be delivered. Confirming a real, working address requires sending a verification message and having the user confirm it. Treat @Email as a cheap first filter against obviously malformed input, not as proof of deliverability.