Bean Validation (JSR 380) is the standard for validating Java objects, providing a declarative way to enforce data integrity constraints through annotations. It's widely used in Java applications, especially with Spring Boot and Jakarta EE.
What is Bean Validation?
Bean Validation allows you to define constraints on object properties using annotations, making validation logic concise, reusable, and maintainable.
Core Dependencies
Maven Dependencies
<dependencies> <!-- Jakarta Validation API --> <dependency> <groupId>jakarta.validation</groupId> <artifactId>jakarta.validation-api</artifactId> <version>3.0.2</version> </dependency> <!-- Hibernate Validator (Reference Implementation) --> <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>8.0.1.Final</version> </dependency> <!-- Expression Language (Required for Hibernate Validator) --> <dependency> <groupId>org.glassfish</groupId> <artifactId>jakarta.el</artifactId> <version>4.0.2</version> </dependency> </dependencies>
Basic Constraint Annotations
Example 1: Simple Entity Validation
import jakarta.validation.constraints.*; import java.time.LocalDate; public class User { @NotNull(message = "ID cannot be null") private Long id; @NotBlank(message = "Name is required") @Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters") private String name; @Email(message = "Email should be valid") @NotBlank(message = "Email is required") private String email; @Min(value = 18, message = "Age must be at least 18") @Max(value = 120, message = "Age must be less than 120") private Integer age; @Pattern(regexp = "^\\+?[1-9]\\d{1,14}$", message = "Phone number must be valid") private String phoneNumber; @Past(message = "Birth date must be in the past") private LocalDate birthDate; @AssertTrue(message = "Must accept terms and conditions") private Boolean acceptedTerms; // Constructors, getters, and setters public User() {} public User(Long id, String name, String email, Integer age, String phoneNumber, LocalDate birthDate, Boolean acceptedTerms) { this.id = id; this.name = name; this.email = email; this.age = age; this.phoneNumber = phoneNumber; this.birthDate = birthDate; this.acceptedTerms = acceptedTerms; } // Getters and setters public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getPhoneNumber() { return phoneNumber; } public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; } public LocalDate getBirthDate() { return birthDate; } public void setBirthDate(LocalDate birthDate) { this.birthDate = birthDate; } public Boolean getAcceptedTerms() { return acceptedTerms; } public void setAcceptedTerms(Boolean acceptedTerms) { this.acceptedTerms = acceptedTerms; } } Programmatic Validation
Example 2: Manual Validation
import jakarta.validation.*; import java.util.Set; public class ManualValidationExample { public static void main(String[] args) { // Create validator factory ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); Validator validator = factory.getValidator(); // Create a user with invalid data User invalidUser = new User( null, // ID is null - invalid "A", // Name too short - invalid "invalid-email", // Invalid email format 16, // Age below minimum - invalid "123", // Invalid phone format LocalDate.now().plusDays(1), // Future date - invalid false // Terms not accepted - invalid ); // Validate the object Set<ConstraintViolation<User>> violations = validator.validate(invalidUser); // Process violations if (!violations.isEmpty()) { System.out.println("Validation errors found:"); for (ConstraintViolation<User> violation : violations) { System.out.printf("- %s: %s%n", violation.getPropertyPath(), violation.getMessage()); } } else { System.out.println("Validation successful!"); } // Close the factory factory.close(); } } Output:
Validation errors found: - id: ID cannot be null - name: Name must be between 2 and 50 characters - email: Email should be valid - age: Age must be at least 18 - phoneNumber: Phone number must be valid - birthDate: Birth date must be in the past - acceptedTerms: Must accept terms and conditions
Advanced Validation Features
Example 3: Custom Validation Annotations
import jakarta.validation.*; import java.lang.annotation.*; import java.time.LocalDate; import java.time.Period; // Custom annotation @Target({ElementType.FIELD, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = AdultValidator.class) public @interface Adult { String message() default "Must be at least 18 years old"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; int value() default 18; } // Custom validator class AdultValidator implements ConstraintValidator<Adult, LocalDate> { private int minAge; @Override public void initialize(Adult constraintAnnotation) { this.minAge = constraintAnnotation.value(); } @Override public boolean isValid(LocalDate birthDate, ConstraintValidatorContext context) { if (birthDate == null) { return true; // Use @NotNull for null checks } return Period.between(birthDate, LocalDate.now()).getYears() >= minAge; } } // Usage in entity class Person { @NotBlank private String name; @Adult(message = "Must be at least 18 years old") private LocalDate birthDate; // Constructors, getters, setters public Person(String name, LocalDate birthDate) { this.name = name; this.birthDate = birthDate; } public String getName() { return name; } public void setName(String name) { this.name = name; } public LocalDate getBirthDate() { return birthDate; } public void setBirthDate(LocalDate birthDate) { this.birthDate = birthDate; } } Example 4: Cross-Field Validation
import jakarta.validation.*; import java.lang.annotation.*; // Cross-field validation annotation @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = PasswordMatchValidator.class) public @interface PasswordMatch { String message() default "Passwords do not match"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; } // Cross-field validator class PasswordMatchValidator implements ConstraintValidator<PasswordMatch, UserRegistration> { @Override public boolean isValid(UserRegistration registration, ConstraintValidatorContext context) { if (registration.getPassword() == null || registration.getConfirmPassword() == null) { return true; // Let @NotNull handle null values } return registration.getPassword().equals(registration.getConfirmPassword()); } } // Entity with cross-field validation @PasswordMatch public class UserRegistration { @NotBlank private String username; @Size(min = 8, message = "Password must be at least 8 characters") private String password; private String confirmPassword; // Constructors, getters, setters public UserRegistration() {} public UserRegistration(String username, String password, String confirmPassword) { this.username = username; this.password = password; this.confirmPassword = confirmPassword; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getConfirmPassword() { return confirmPassword; } public void setConfirmPassword(String confirmPassword) { this.confirmPassword = confirmPassword; } } Validation Groups
Example 5: Group-based Validation
import jakarta.validation.constraints.*; import jakarta.validation.groups.Default; // Validation groups public interface CreateGroup {} public interface UpdateGroup {} public class Product { @NotNull(groups = UpdateGroup.class, message = "ID is required for update") private Long id; @NotBlank(groups = {CreateGroup.class, Default.class}, message = "Name is required") @Size(min = 2, max = 100, groups = {CreateGroup.class, Default.class}) private String name; @NotNull(groups = {CreateGroup.class, Default.class}) @DecimalMin(value = "0.0", inclusive = false, groups = {CreateGroup.class, Default.class}) private Double price; @Min(value = 0, groups = {CreateGroup.class, Default.class}) private Integer stock; // Constructors, getters, setters public Product() {} public Product(Long id, String name, Double price, Integer stock) { this.id = id; this.name = name; this.price = price; this.stock = stock; } // Getters and setters... } // Usage with groups public class GroupValidationExample { public static void main(String[] args) { Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); // Create product for creation (ID can be null) Product newProduct = new Product(null, "Laptop", 999.99, 10); // Validate for creation Set<ConstraintViolation<Product>> createViolations = validator.validate(newProduct, CreateGroup.class); System.out.println("Create violations: " + createViolations.size()); // Create product for update (ID cannot be null) Product updateProduct = new Product(null, "Laptop", 999.99, 10); // Validate for update Set<ConstraintViolation<Product>> updateViolations = validator.validate(updateProduct, UpdateGroup.class); System.out.println("Update violations: " + updateViolations.size()); updateViolations.forEach(v -> System.out.println("- " + v.getMessage())); } } Spring Boot Integration
Example 6: Spring REST Controller Validation
import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import jakarta.validation.Valid; @RestController @RequestMapping("/api/users") public class UserController { @PostMapping public ResponseEntity<User> createUser(@Valid @RequestBody User user) { // If validation fails, Spring automatically returns 400 Bad Request // with detailed error messages // Process valid user User savedUser = userService.save(user); return ResponseEntity.ok(savedUser); } @PutMapping("/{id}") public ResponseEntity<User> updateUser( @PathVariable Long id, @Valid @RequestBody User user) { user.setId(id); User updatedUser = userService.update(user); return ResponseEntity.ok(updatedUser); } } Example 7: Custom Error Handling in Spring
import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import jakarta.validation.ConstraintViolationException; import java.util.HashMap; import java.util.Map; @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<Map<String, String>> handleValidationExceptions( MethodArgumentNotValidException ex) { Map<String, String> errors = new HashMap<>(); ex.getBindingResult().getFieldErrors().forEach(error -> errors.put(error.getField(), error.getDefaultMessage())); return ResponseEntity.badRequest().body(errors); } @ExceptionHandler(ConstraintViolationException.class) public ResponseEntity<Map<String, String>> handleConstraintViolation( ConstraintViolationException ex) { Map<String, String> errors = new HashMap<>(); ex.getConstraintViolations().forEach(violation -> errors.put(violation.getPropertyPath().toString(), violation.getMessage())); return ResponseEntity.badRequest().body(errors); } } Collection Validation
Example 8: Validating Collections and Nested Objects
import jakarta.validation.Valid; import jakarta.validation.constraints.*; import java.util.List; public class Order { @NotNull private Long id; @NotBlank private String customerName; @Valid // Important: Triggers validation of nested objects @NotEmpty(message = "Order must have at least one item") private List<OrderItem> items; @Valid private Address shippingAddress; // Constructors, getters, setters public Order() {} public Order(Long id, String customerName, List<OrderItem> items, Address shippingAddress) { this.id = id; this.customerName = customerName; this.items = items; this.shippingAddress = shippingAddress; } // Getters and setters... } class OrderItem { @NotBlank private String productName; @Min(1) private Integer quantity; @DecimalMin("0.0") private Double price; // Constructors, getters, setters... } class Address { @NotBlank private String street; @NotBlank private String city; @NotBlank @Size(min = 2, max = 50) private String country; @Pattern(regexp = "^[0-9]{5}(-[0-9]{4})?$") private String zipCode; // Constructors, getters, setters... } Conditional Validation
Example 9: Conditional Validation with @Assert
import jakarta.validation.constraints.*; public class Payment { public enum PaymentMethod { CREDIT_CARD, PAYPAL, BANK_TRANSFER } @NotNull private PaymentMethod paymentMethod; // Only required for credit card payments @NotBlank(groups = CreditCardGroup.class) private String cardNumber; // Only required for credit card payments @NotBlank(groups = CreditCardGroup.class) private String expiryDate; // Only required for PayPal @NotBlank(groups = PayPalGroup.class) private String paypalEmail; // Only required for bank transfer @NotBlank(groups = BankTransferGroup.class) private String bankAccount; // Conditional validation groups public interface CreditCardGroup {} public interface PayPalGroup {} public interface BankTransferGroup {} @AssertTrue(message = "Payment details are incomplete") private boolean isPaymentDetailsComplete() { switch (paymentMethod) { case CREDIT_CARD: return cardNumber != null && !cardNumber.trim().isEmpty() && expiryDate != null && !expiryDate.trim().isEmpty(); case PAYPAL: return paypalEmail != null && !paypalEmail.trim().isEmpty(); case BANK_TRANSFER: return bankAccount != null && !bankAccount.trim().isEmpty(); default: return false; } } // Constructors, getters, setters... } Best Practices
1. Use Meaningful Error Messages
@NotBlank(message = "Username is required and cannot be empty") private String username; @Email(message = "Please provide a valid email address") private String email;
2. Combine Annotations Appropriately
// Good: Clear and specific @NotBlank @Size(min = 8, max = 20) @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).*$") private String password;
3. Validate at Multiple Layers
@Service @Validated // Enables method-level validation public class UserService { public User createUser(@Valid User user) { // Business logic here return userRepository.save(user); } public User updateUser(@NotNull Long id, @Valid User user) { // Business logic here return userRepository.save(user); } } Common Constraint Annotations Reference
| Annotation | Purpose | Example |
|---|---|---|
@NotNull | Not null | @NotNull private String value; |
@NotBlank | Not null/empty/whitespace | @NotBlank private String name; |
@NotEmpty | Not null/empty | @NotEmpty private List<String> items; |
@Size | Size range | @Size(min=2, max=50) private String name; |
@Min / @Max | Numeric range | @Min(18) private Integer age; |
@Email | Email format | @Email private String email; |
@Pattern | Regex pattern | @Pattern(regexp="^[A-Za-z]+$") |
@Past / @Future | Date constraints | @Past private LocalDate birthDate; |
@Positive / @Negative | Sign constraints | @Positive private BigDecimal amount; |
@DecimalMin / @DecimalMax | Decimal range | @DecimalMin("0.0") private Double price; |
Conclusion
Bean Validation provides a powerful, declarative approach to input validation in Java:
Key Benefits:
- Declarative Syntax - Annotations make validation rules clear and concise
- Reusability - Validation logic is centralized and reusable
- Integration - Seamless integration with Spring, Jakarta EE, and other frameworks
- Extensibility - Easy to create custom validators
- Type Safety - Compile-time checking of constraint usage
Best Practices:
- Validate early and often
- Use meaningful error messages
- Combine framework validation with business logic validation
- Use validation groups for different scenarios
- Test your validation rules thoroughly
Bean Validation is essential for building robust, maintainable Java applications that properly handle user input and maintain data integrity.
Next Steps: Explore Spring Boot's validation starter for automatic configuration, and consider integrating with internationalization (i18n) for localized error messages.