google-site-verification: google61fe8ba583a51912.html
Input Validation with Bean Validation in Java

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

AnnotationPurposeExample
@NotNullNot null@NotNull private String value;
@NotBlankNot null/empty/whitespace@NotBlank private String name;
@NotEmptyNot null/empty@NotEmpty private List<String> items;
@SizeSize range@Size(min=2, max=50) private String name;
@Min / @MaxNumeric range@Min(18) private Integer age;
@EmailEmail format@Email private String email;
@PatternRegex pattern@Pattern(regexp="^[A-Za-z]+$")
@Past / @FutureDate constraints@Past private LocalDate birthDate;
@Positive / @NegativeSign constraints@Positive private BigDecimal amount;
@DecimalMin / @DecimalMaxDecimal 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:

  1. Validate early and often
  2. Use meaningful error messages
  3. Combine framework validation with business logic validation
  4. Use validation groups for different scenarios
  5. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper