Bean Validation — declarative input data validation using annotations. Spring Boot integrates Hibernate Validator.
Simple analogy: Validation is like checking a passport at the border. You show your passport (data), the border guard checks annotations (@NotBlank, @Email) — lets you through or detains you. Without validation, any garbage input reaches your business logic, leading to cryptic database errors, security vulnerabilities, and corrupted data.
Why it matters: Input validation is the first line of defense against bad data and security attacks (SQL injection, XSS). Declarative annotations (@NotBlank, @Email) replace dozens of manual if checks with readable, self-documenting rules. It also separates validation from business logic — the controller validates the shape, the service processes the data.
Dependency:
1<dependency>2 <groupId>org.springframework.boot</groupId>3 <artifactId>spring-boot-starter-validation</artifactId>4</dependency>
DTO with annotations:
1public record CreateUserRequest(2 @NotBlank(message = "Name is required")3 @Size(min = 2, max = 100, message = "Name must be 2 to 100 characters")4 String name,56 @NotBlank @Email(message = "Invalid email")7 String email,89 @NotBlank @Size(min = 8, message = "Password must be at least 8 characters")10 @Pattern(regexp = ".*[A-Z].*", message = "Password must contain an uppercase letter")11 String password,1213 @Min(value = 18, message = "Minimum age is 18")14 @Max(value = 120)15 int age,1617 @Past(message = "Birthday cannot be in the future")18 LocalDate birthday19) {}
In the controller:
1@RestController2@RequestMapping("/api/users")3public class UserController {4 @PostMapping5 public ResponseEntity<User> create(@Valid @RequestBody CreateUserRequest request) {6 return ResponseEntity.ok(service.create(request));7 }8}
Custom validation:
1@Target({ElementType.FIELD})2@Retention(RetentionPolicy.RUNTIME)3@Constraint(validatedBy = PhoneValidator.class)4public @interface PhoneNumber {5 String message() default "Invalid phone number";6 Class<?>[] groups() default {};7 Class<? extends Payload>[] payload() default {};8}910public class PhoneValidator implements ConstraintValidator<PhoneNumber, String> {11 private static final Pattern PHONE_PATTERN =12 Pattern.compile("^\\+?[0-9]{10,15}$");1314 @Override15 public void initialize(PhoneNumber annotation) { }1617 @Override18 public boolean isValid(String value, ConstraintValidatorContext ctx) {19 return value == null || PHONE_PATTERN.matcher(value).matches();20 }21}2223// Usage24@PhoneNumber25private String phone;
Validation groups allow you to check different sets of rules in different scenarios:
1public interface ValidationGroups {2 interface Create {}3 interface Update {}4}56public record UserRequest(7 @Null(groups = ValidationGroups.Create.class, message = "ID must be null on create")8 @NotNull(groups = ValidationGroups.Update.class)9 Long id,1011 @NotBlank12 String name13) {}1415// In controller:16@PostMapping17public ResponseEntity<?> create(@Validated(ValidationGroups.Create.class)18 @RequestBody UserRequest request) { ... }
Common mistakes:
@Valid on @RequestBody — validation annotations are silently ignored without it.