Bean Validation
Definition
Bean Validation lets you put rules on fields with annotations such as @NotBlank and @Size. Adding @Valid to a controller parameter checks them and gives you clear field errors.
Why it exists
Bad input that reaches business code fails in confusing places or corrupts data. Declaring rules next to the fields keeps them visible and gives callers precise errors.
How it works
Groups
Groups apply different rules for create versus update:
interface OnCreate {}
interface OnUpdate {}
record UserDto(
@Null(groups = OnCreate.class) @NotNull(groups = OnUpdate.class) Long id,
@NotBlank String name) {
}
Use @Validated(OnCreate.class) on the parameter. Often separate request DTOs for create and update are simpler than groups.
Example code
Validating a request body
public record CreateOrderRequest(
@NotNull Long customerId,
@NotEmpty @Size(max = 50) List<@Valid OrderLine> lines,
@Email String notificationEmail) {
public record OrderLine(
@NotBlank String sku,
@Positive int quantity) {
}
}
@RestController
@RequestMapping("/orders")
class OrderController {
@PostMapping
ResponseEntity<Void> create(@Valid @RequestBody CreateOrderRequest request) {
// request is valid here
return ResponseEntity.accepted().build();
}
}
@Valid on the parameter triggers validation; @Valid on OrderLine inside the list cascades into each element. Without the cascade, nested fields are silently unchecked, which is a classic bug.
Validating path variables and request parameters
Put @Validated on the controller class, then use constraints directly on parameters:
@Validated
@RestController
class ProductController {
@GetMapping("/products/{id}")
Product get(@PathVariable @Min(1) long id,
@RequestParam @Size(min = 2, max = 20) String locale) {
// ...
return null;
}
}
Failures here throw ConstraintViolationException (or HandlerMethodValidationException in Spring 6.1+ when using method validation built into MVC). Body failures throw MethodArgumentNotValidException.
Returning field errors
@RestControllerAdvice
class ValidationHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
ProblemDetail onInvalid(MethodArgumentNotValidException ex) {
ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Validation failed");
Map<String, String> errors = new LinkedHashMap<>();
ex.getBindingResult().getFieldErrors()
.forEach(e -> errors.putIfAbsent(e.getField(), e.getDefaultMessage()));
pd.setProperty("errors", errors);
return pd;
}
}
If your global advice already extends ResponseEntityExceptionHandler (as in exception handling), do not add a second advice for the same exception. Override handleMethodArgumentNotValid in that class instead, so only one handler owns the response.
Custom constraint
@Documented
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = SkuValidator.class)
public @interface ValidSku {
String message() default "SKU must look like ABC-12345";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class SkuValidator implements ConstraintValidator<ValidSku, String> {
private static final Pattern PATTERN = Pattern.compile("[A-Z]{3}-\\d{5}");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value == null || PATTERN.matcher(value).matches(); // null handled by @NotNull
}
}
Convention: constraint validators treat null as valid and let @NotNull handle presence. Validators can inject Spring beans (for example to check uniqueness), but keep them cheap; uniqueness must still be enforced by a database constraint because of race conditions.
System design considerations
- Back important rules with database constraints too, since the database is the last line of defence
- Return a stable error format (ProblemDetail) that clients can parse
- Validate messages from queues as strictly as HTTP input
When to use it, and when not
Use it when
- You accept external input (REST bodies, parameters, messages)
- You return consistent field-level errors
- You enforce a rule that several endpoints share (custom constraint)
Think twice when
- The rule needs database state or heavy logic, which belongs in the service layer
- You would rely on validation annotations as your only integrity guarantee
- You expose entities directly instead of request DTOs
Common mistakes
- Missing
@Validon nested objects or collection elements. - Using
@NotNullon primitives (they can never be null). - Mutating entities directly from request DTOs and validating the entity afterwards.
- Relying on application-level uniqueness checks without a database unique index.
Interview questions
1. @Valid vs @Validated?
@Valid is the standard Jakarta annotation: triggers validation and cascades into nested objects. @Validated is Spring's variant: supports validation groups and, on a class, enables method-level validation of parameters like @PathVariable.
2. @NotNull vs @NotEmpty vs @NotBlank?
@NotNull: not null. @NotEmpty: not null and size greater than 0 (strings, collections, maps, arrays). @NotBlank: strings only, not null and has at least one non-whitespace character.
3. Is validation in the controller enough?
No. Validate at the edge for fast feedback, but enforce invariants in the domain and database too (unique indexes, not-null columns, foreign keys), because other entry points such as messages and batch jobs bypass the controller.
4. How do you validate a List<String> of emails?
Container element constraints: List<@Email String>.
5. What exception does a failed @RequestBody validation throw, and how do you handle it?
MethodArgumentNotValidException. Handle it in a @RestControllerAdvice and return 400 with the field errors.
Check yourself
1What makes a @RequestBody validated in a controller?
2Which exception does Spring MVC raise when body validation fails?
3How do you validate @PathVariable and @RequestParam?
Key takeaways
- Annotate DTOs with constraints, trigger with
@Valid, cascade explicitly. - Add
@Validatedon classes for parameter-level constraints. - Return structured field errors with a single advice class.
- Validation at the edge complements, never replaces, database constraints.
Next: Testing basics.
Next topicREST exception handlingFinished this page? Track your progress (saved only in your browser).