Validation in Spring
Spring integrates Bean Validation (JSR 303/380) via annotations such as @NotNull, @Size, @Email. Validation is triggered by @Valid or @Validated on method parameters. This article explains usage, common annotations, validation groups, custom validators, and exception handling with a reference table.
Overview
- Dependency:
spring-boot-starter-validation(includes Hibernate Validator). - Usage: Add validation annotations to DTO fields; add @Valid or @Validated to Controller (or Service) parameters. On failure,
MethodArgumentNotValidExceptionis thrown; handle globally and return 400 with field errors. - Groups: @Validated(Group.class) validates by group; different scenarios (create vs update) can use different groups.
- Custom: @Constraint for custom annotations; implement
ConstraintValidatorfor the validation logic.
Example
Example 1: Common annotations
Javapublic class CreateUserRequest { @NotNull @Size(min = 2, max = 50) private String name; @Email private String email; @Min(0) @Max(150) private Integer age; @Pattern(regexp = "^[A-Z]{2}[0-9]{2}$") private String countryCode; }
- Annotations declare constraints; the validator runs when @Valid or @Validated is present.
Example 2: Controller trigger
Java@PostMapping("/users") public User create(@RequestBody @Valid CreateUserRequest req) { return userService.create(req); }
- @Valid on @RequestBody triggers validation before the method runs. If validation fails, the method is not invoked.
Example 3: Method parameter validation
Java@Validated @Service public class UserService { public void update(@NotNull @Min(1) Long id, @Valid UpdateRequest req) { // ... } }
- @Validated on the class enables method parameter validation (e.g. @NotNull, @Min on primitive/wrapper params). Without @Validated, only @Valid on object parameters works.
Example 4: Validation groups
Javapublic interface Create {} public interface Update {} public class UserRequest { @NotNull(groups = Update.class) // Only for update private Long id; @NotBlank(groups = {Create.class, Update.class}) private String name; } @PostMapping("/users") public User create(@RequestBody @Validated(Create.class) UserRequest req) { } @PutMapping("/users/{id}") public User update(@RequestBody @Validated(Update.class) UserRequest req) { }
- Groups let you validate different subsets of constraints for different endpoints or scenarios.
Example 5: Custom validator
Java@Constraint(validatedBy = UniqueEmailValidator.class) @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface UniqueEmail { String message() default "Email already exists"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; } public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> { @Autowired private UserRepository userRepo; @Override public boolean isValid(String email, ConstraintValidatorContext ctx) { return email == null || !userRepo.existsByEmail(email); } }
- For uniqueness and other DB-dependent checks, custom validators are needed. Format and range checks use built-in annotations.
Example 6: Global exception handler
Java@RestControllerAdvice public class ValidationHandler { @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<Map<String, Object>> handle(MethodArgumentNotValidException ex) { Map<String, String> errors = new HashMap<>(); ex.getBindingResult().getFieldErrors().forEach(e -> errors.put(e.getField(), e.getDefaultMessage())); return ResponseEntity.badRequest().body(Map.of("errors", errors)); } }
- Return 400 with field-level errors so the client can display them. Include field name and message for each violation.
Example 7: Common annotations
| Annotation | Description |
|---|---|
| @NotNull | Not null |
| @NotBlank | Non-null, non-empty string (trimmed) |
| @NotEmpty | Not null, not empty (string, collection, array) |
| @Size | Length or size in range |
| Email format | |
| @Pattern | Regex match |
| @Min / @Max | Numeric range |
| @Positive / @PositiveOrZero | Number constraints |
| @Future / @Past | Date constraints |
Core Mechanism / Behavior
- @Valid: Triggers validation and recursive validation of nested objects (e.g. nested DTOs).
- @Validated: Enables method parameter validation and supports groups. Must be on the class for method validation.
- Exception:
MethodArgumentNotValidExceptionholdsBindingResultwith field and global errors. Extract and return in the response. - Order: Validation runs before the method; if it fails, the method is not called.
Validation vs Authorization
- Validation: Checks format, range, required fields. "Is this a valid email? Is age in range?" Use Bean Validation.
- Authorization: Checks permissions. "Can this user access this resource?" Use Spring Security, custom checks in Service.
- Do not use validation annotations for authorization; keep them separate. Validation runs early; authorization may depend on loaded entities and context.
Common Pitfalls
- Forgetting @Validated on class: Method parameter validation (e.g. @NotNull on Long id) requires @Validated on the class.
- Nested validation: Nested objects need @Valid; without it, nested constraints are not checked.
- Custom validator with DI: ConstraintValidator can receive injected dependencies; ensure the validator is managed by Spring (e.g. use
ConstraintValidatorFactory).
Key Rules
- @Valid for recursive validation; @Validated for method params and groups.
- Global exception handler for validation failures; return 400 with structured field errors.
- Format and required fields: Use annotations. Uniqueness and business rules: Use custom validators or Service layer + DB.
- Do not rely on validation alone for security; validate for correctness and UX; enforce authorization and integrity in the service layer.
What's Next
See Parameter Binding for argument handling. See API Gateway for upfront validation at the edge.