What is Bean Validation?
Think of Bean Validation like airport security checks:
- ✈️ Data enters your application (passengers boarding)
- 🔍 Validation checks the data (security screening)
- ❌ Invalid data is rejected (denied boarding)
- ✅ Valid data proceeds (cleared for flight)
Bean Validation allows you to define validation rules on your Java objects using annotations. No manual if-else checks needed!
Reference implementation: Hibernate Validator
Without Bean Validation (Manual Checks)
public void registerUser(User user) {
if (user.getUsername() == null || user.getUsername().isEmpty()) {
throw new IllegalArgumentException("Username required");
}
if (user.getUsername().length() < 3) {
throw new IllegalArgumentException("Username too short");
}
if (user.getEmail() == null || !user.getEmail().contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
if (user.getAge() < 18) {
throw new IllegalArgumentException("Must be 18+");
}
// So much boilerplate!
}
With Bean Validation (Clean!)
public class User {
@NotBlank(message = "Username required")
@Size(min = 3, max = 50)
private String username;
@Email
@NotNull
private String email;
@Min(18)
private int age;
}
// Validation happens automatically!
Built-In Validation Constraints
Null/Empty Checks
public class Person {
@NotNull // Must not be null
private String firstName;
@NotBlank // Not null, not empty, not just whitespace
private String lastName;
@NotEmpty // Not null and size > 0
private List<String> hobbies;
@Null // Must be null (rare, for conditional validation)
private String internalField;
}
Size/Range Constraints
public class Product {
@Size(min = 5, max = 100) // String length or collection size
private String name;
@Min(0) // Minimum value
@Max(1000) // Maximum value
private double price;
@DecimalMin("0.0") // For BigDecimal
@DecimalMax("999.99")
private BigDecimal discount;
@Positive // > 0
private int quantity;
@PositiveOrZero // >= 0
private int stock;
@Negative // < 0
private int deficit;
}
Pattern/Format Constraints
public class Contact {
@Email // Valid email format
private String email;
@Pattern(regexp = "^\\d{3}-\\d{3}-\\d{4}$",
message = "Phone must be in format XXX-XXX-XXXX")
private String phone;
@Pattern(regexp = "^[A-Z]{2}\\d{6}$") // Passport format
private String passport;
}
Date/Time Constraints
public class Event {
@Past // Must be in the past
private LocalDate birthDate;
@PastOrPresent // Past or today
private LocalDate registrationDate;
@Future // Must be in the future
private LocalDate eventDate;
@FutureOrPresent // Future or today
private LocalDate deadline;
}
Boolean Constraints
public class Agreement {
@AssertTrue // Must be true
private boolean termsAccepted;
@AssertFalse // Must be false
private boolean fraudulent;
}
Validating Objects
Manual Validation
import jakarta.validation.*;
import java.util.*;
public class ValidationExample {
public static void main(String[] args) {
// Get validator instance
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
// Create object with invalid data
User user = new User();
user.setUsername("ab"); // Too short!
user.setEmail("invalid"); // Invalid email!
user.setAge(15); // Too young!
// Validate
Set<ConstraintViolation<User>> violations = validator.validate(user);
// Check violations
if (!violations.isEmpty()) {
for (ConstraintViolation<User> violation : violations) {
System.out.println(violation.getPropertyPath() + ": " +
violation.getMessage());
}
}
}
}
Output:
username: size must be between 3 and 50
email: must be a well-formed email address
age: must be greater than or equal to 18
Automatic Validation in Jakarta EE
@Path("/users")
public class UserResource {
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response createUser(@Valid User user) { // @Valid triggers validation!
// If validation fails, ConstraintViolationException is thrown
// If valid, continues here
userService.save(user);
return Response.ok().build();
}
}
Nested Validation
public class Order {
@NotNull
@Size(min = 1)
@Valid // Cascade validation to items!
private List<OrderItem> items;
@NotNull
@Valid // Validate address too
private Address shippingAddress;
}
public class OrderItem {
@NotNull
private String productName;
@Min(1)
private int quantity;
@Positive
private double price;
}
public class Address {
@NotBlank
private String street;
@NotBlank
private String city;
@Pattern(regexp = "^\\d{5}$")
private String zipCode;
}
// Validates Order, all OrderItems, and Address!
Custom Validation Constraints
1. Define the Annotation
import jakarta.validation.*;
import java.lang.annotation.*;
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = StrongPasswordValidator.class)
public @interface StrongPassword {
String message() default "Password must be strong";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
2. Create the Validator
import jakarta.validation.*;
public class StrongPasswordValidator
implements ConstraintValidator<StrongPassword, String> {
@Override
public boolean isValid(String password, ConstraintValidatorContext context) {
if (password == null) {
return false;
}
// Must be at least 8 characters
if (password.length() < 8) {
return false;
}
// Must contain uppercase, lowercase, digit, special char
boolean hasUppercase = password.matches(".*[A-Z].*");
boolean hasLowercase = password.matches(".*[a-z].*");
boolean hasDigit = password.matches(".*\\d.*");
boolean hasSpecial = password.matches(".*[!@#$%^&*].*");
return hasUppercase && hasLowercase && hasDigit && hasSpecial;
}
}
3. Use It
public class User {
@NotBlank
private String username;
@StrongPassword // Custom constraint!
private String password;
}
Validation Groups
Apply different validation rules in different scenarios
// Define group interfaces
public interface Create { }
public interface Update { }
public class User {
@Null(groups = Create.class) // ID must be null when creating
@NotNull(groups = Update.class) // ID required when updating
private Long id;
@NotBlank // Always required (default group)
private String username;
@NotNull(groups = Create.class) // Password required on create
private String password;
}
// Validate with specific group
Set<ConstraintViolation<User>> violations =
validator.validate(user, Create.class); // Validate for creation
In REST APIs
@Path("/users")
public class UserResource {
@POST
public Response create(@Valid @ConvertGroup(to = Create.class) User user) {
// Validates using Create group
return Response.ok().build();
}
@PUT
@Path("/{id}")
public Response update(@Valid @ConvertGroup(to = Update.class) User user) {
// Validates using Update group
return Response.ok().build();
}
}
Method Validation
@ApplicationScoped
@Validated // Enable method validation
public class UserService {
// Validate parameters
public void createUser(@Valid @NotNull User user) {
// user is validated before method executes
}
// Validate individual parameters
public User findByEmail(@Email @NotBlank String email) {
// email is validated
return null;
}
// Validate return value
@NotNull
public User getCurrentUser() {
// Must return non-null User
return new User();
}
}
Custom Error Messages
Inline Messages
public class Product {
@NotBlank(message = "Product name is required")
private String name;
@Min(value = 0, message = "Price must be positive")
private double price;
@Size(min = 10, max = 500,
message = "Description must be between {min} and {max} characters")
private String description;
}
Internationalization (i18n)
// ValidationMessages.properties
product.name.required=Product name is required
product.price.positive=Price must be at least ${value}
// Use in code
@NotBlank(message = "{product.name.required}")
private String name;
@Min(value = 0, message = "{product.price.positive}")
private double price;
Best Practices
✅ DO:
- Use @Valid for nested objects - Cascade validation
- Provide clear error messages - Users need to understand what's wrong
- Validate at boundaries - REST endpoints, form submissions
- Use groups for different scenarios - Create vs Update
- Combine multiple constraints - @NotNull + @Size + @Pattern
- Create custom validators for complex rules - Reusable logic
- Validate method parameters - Protect service layer
❌ DON'T:
- Don't skip validation on updates - Always validate
- Don't validate too late - Fail fast at entry points
- Don't mix validation and business logic - Keep separate
- Don't use generic messages - Be specific
- Don't forget @Valid on nested objects - Won't validate otherwise
- Don't overuse custom validators - Use built-in when possible
Summary
- Bean Validation validates data using annotations
- Built-in constraints: @NotNull, @NotBlank, @Size, @Email, @Min, @Max, @Pattern
- @Valid: Triggers validation (REST endpoints, nested objects)
- Custom constraints: Create your own validation annotations
- Validation groups: Different rules for different scenarios
- Method validation: Validate parameters and return values
- Nested validation: Cascade validation with @Valid
- Automatic in Jakarta EE: Works with JAX-RS, JPA, CDI
- Benefits: Declarative, reusable, consistent validation
- Implementation: Hibernate Validator (reference implementation)