Bean Validation Intro
Input you don’t validate is a bug waiting to happen. Jakarta Bean Validation is the standard, declarative way to enforce constraints on Java objects — you annotate fields with rules like @NotBlank or @Email, and the framework checks them for you. Spring Boot wires this in automatically, so a single @Valid annotation on a controller method rejects bad requests before your business logic ever runs.
Why declarative validation
Without validation you end up writing the same defensive if checks at the top of every method:
if (request.name() == null || request.name().isBlank()) {
throw new IllegalArgumentException("name is required");
}
if (request.age() < 0) {
throw new IllegalArgumentException("age must be positive");
}
That code is repetitive, easy to forget, and scattered across the codebase. Bean Validation moves these rules onto the data itself as annotations, so the constraint lives next to the field it protects and is enforced consistently everywhere the object is validated.
public record CreateUserRequest(
@NotBlank String name,
@Positive int age) {}
The specification and its implementation
It helps to separate two things:
| Layer | What it is | Example |
|---|---|---|
| Jakarta Bean Validation | The specification (JSR 380 / Jakarta Validation 3.0) — defines the annotations and API | jakarta.validation.constraints.* |
| Hibernate Validator | The reference implementation that actually evaluates the constraints | org.hibernate.validator |
You program against the standard jakarta.validation annotations; Hibernate Validator does the work behind the scenes. The two are independent of Hibernate ORM despite the shared name.
Warning: Always import from
jakarta.validation.constraints, never the legacyjavax.validation.*. Spring Boot 3.x runs on Jakarta EE 9+, and the oldjavaxpackages will not be on the classpath.
Adding the dependency
The starter pulls in the API and Hibernate Validator together. It is not included by spring-boot-starter-web by default in Spring Boot 3.x, so you must add it explicitly.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Gradle:
implementation 'org.springframework.boot:spring-boot-starter-validation'
With spring-boot-starter-parent 3.5.x managing versions, you don’t specify a version yourself.
@Valid in a controller
Annotate a @RequestBody parameter with @Valid and Spring validates it before invoking the method body. If any constraint fails, the request is rejected with 400 Bad Request and the handler never runs.
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import org.springframework.web.bind.annotation.*;
public record CreateUserRequest(
@NotBlank String name,
@Email String email,
@Min(18) int age) {}
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public UserResponse create(@Valid @RequestBody CreateUserRequest request) {
return service.create(request); // only reached if validation passes
}
}
Invalid request:
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name":"","email":"not-an-email","age":12}'
Output (default Spring Boot error):
{
"timestamp": "2026-06-13T10:15:30.123+00:00",
"status": 400,
"error": "Bad Request",
"path": "/api/users"
}
That default response is terse — you’ll almost always want to translate failures into a clean, field-level JSON payload. See Handling Validation Errors.
How it integrates
Spring Boot auto-configures a LocalValidatorFactoryBean (a Validator) whenever the starter is on the classpath. Two mechanisms then drive validation:
- For request bodies, the
RequestResponseBodyMethodProcessorhonors@Validand throwsMethodArgumentNotValidExceptionon failure. - For method parameters like
@RequestParamand@PathVariable, theMethodValidationPostProcessorkicks in when the class is annotated@Validated, throwingConstraintViolationException.
You can also inject the Validator and validate any object programmatically:
@Service
@RequiredArgsConstructor
public class ImportService {
private final jakarta.validation.Validator validator;
public void process(CreateUserRequest req) {
var violations = validator.validate(req);
if (!violations.isEmpty()) {
throw new jakarta.validation.ConstraintViolationException(violations);
}
}
}
Tip:
@Valid(fromjakarta.validation) triggers standard validation and cascades into nested objects.@Validated(from Spring) additionally supports validation groups and enables method-level parameter validation.
In This Section
- Common Constraints — the standard annotation reference (
@NotNull,@Size,@Email, …) - Validating Request Bodies —
@Validon@RequestBody, nested objects, collections - Validating Path & Query Params —
@Validatedon the controller,@ConfigurationProperties - Custom Validators — your own constraint annotations and
ConstraintValidator - Validation Groups — different rules for create vs update
- Handling Validation Errors — clean JSON error responses with
ProblemDetail