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