Bean Validation

Validating Data with Annotations

← Back to Index

What is Bean Validation?

Think of Bean Validation like airport security checks:

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)