Bean Validation with @Valid
IntermediateJSR-303/380 constraints (@NotNull, @Size, @Email) on request DTOs are enforced automatically when @Valid is placed on the method parameter.
Overview
Spring Boot integrates Jakarta Bean Validation (formerly JSR-303/380) via `spring-boot-starter-validation`, which pulls in Hibernate Validator as the reference implementation. Place constraint annotations (`@NotNull`, `@Size`, `@Email`, `@Pattern`, `@Min`, `@Max`, `@Positive`) directly on your request DTO fields. Add `@Valid` (or `@Validated` for group validation) to the controller method parameter and Spring MVC automatically validates before invoking the method. Violations produce a `MethodArgumentNotValidException`, which you should map to a structured 400 response with a global `@ControllerAdvice`. Custom constraints are defined by creating an annotation plus a `ConstraintValidator<A, T>` implementation.
Constraint Annotations on DTOs
Annotate your request DTO with constraint annotations. Nested objects must be annotated with `@Valid` themselves for cascaded validation. `@NotNull` checks for non-null; `@NotBlank` checks non-null AND non-empty string (whitespace-stripped); `@Size` validates collection and string lengths.
public record CreateUserRequest(
@NotBlank(message = "Name is required")
@Size(max = 100, message = "Name must be at most 100 characters")
String name,
@NotNull @Email(message = "Must be a valid email address")
String email,
@NotNull @Min(18) @Max(120)
Integer age,
@Valid // cascade validation into the nested object
@NotNull
AddressRequest address
) {}
public record AddressRequest(
@NotBlank String street,
@Pattern(regexp = "^[0-9]{5}$", message = "ZIP must be 5 digits")
String zip
) {}@Valid in Controllers and Handling Errors
Placing `@Valid` on the `@RequestBody` parameter triggers validation before the method body executes. A `MethodArgumentNotValidException` is thrown on failure. Handle it in a `@ControllerAdvice` to return a structured error response — never let the default Spring error page leak to clients.
@RestController
@RequestMapping("/users")
class UserController {
@PostMapping
ResponseEntity<UserResponse> create(@Valid @RequestBody CreateUserRequest req) {
// Only reached if validation passes
return ResponseEntity.status(201).body(userService.create(req));
}
}
// Global error handler
@RestControllerAdvice
class ValidationExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
Map<String, Object> handleValidation(MethodArgumentNotValidException ex) {
List<String> errors = ex.getBindingResult().getFieldErrors().stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.toList();
return Map.of("status", 400, "errors", errors);
}
}Custom Constraints and @Validated Groups
For business-rule validations (e.g., unique email, cross-field checks), create a custom `@Constraint` annotation + `ConstraintValidator`. `@Validated` with groups selects which subset of constraints to apply — useful when the same DTO is used for both Create (requires ID-less fields) and Update (requires ID).
// Custom constraint annotation
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueEmailValidator.class)
public @interface UniqueEmail {
String message() default "Email already registered";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// Validator logic
@Component
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
@Autowired UserRepository repo;
@Override
public boolean isValid(String email, ConstraintValidatorContext ctx) {
return email == null || !repo.existsByEmail(email);
}
}
// Validation groups
interface OnCreate {}
interface OnUpdate {}
public record UserRequest(
@NotNull(groups = OnUpdate.class) Long id,
@NotBlank @UniqueEmail(groups = OnCreate.class) String email
) {}
// Controller uses @Validated(OnCreate.class) instead of @Valid
@PostMapping
ResponseEntity<?> create(@Validated(OnCreate.class) @RequestBody UserRequest req) { ... }Key Points to Remember
- 1Add spring-boot-starter-validation to get Hibernate Validator on the classpath
- 2@Valid on @RequestBody triggers validation; @Validated adds group support
- 3@NotBlank is stricter than @NotNull — it also rejects empty/whitespace strings
- 4Nested objects require their own @Valid annotation for cascaded validation
- 5Handle MethodArgumentNotValidException in @ControllerAdvice for structured 400 responses
- 6Custom constraints: create a @Constraint annotation + ConstraintValidator<A, T> class
Interview Questions
Sign in to ask AriaWhat is the difference between @Valid and @Validated in Spring?
What is the difference between @NotNull, @NotEmpty, and @NotBlank?
How would you return a structured JSON error response when validation fails?
How do you create a custom constraint annotation in Spring Boot?
How do validation groups help when the same DTO is used for Create and Update?
Ask Aria about Bean Validation with @Valid
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.