google-site-verification: google61fe8ba583a51912.html
Hibernate Validator Custom Constraints in Java

Hibernate Validator provides a powerful framework for validating Java objects. While it comes with many built-in constraints, you can create custom constraints to handle specific business validation rules.


Why Custom Constraints?

  • Domain-specific validation: Business rules unique to your application
  • Complex validation logic: Multi-field validation or conditional rules
  • Reusable validation: Apply the same rules across multiple classes
  • Cleaner code: Keep validation logic separate from business logic

Project Setup

Maven Dependencies

<dependencies> <!-- Hibernate Validator --> <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>8.0.1.Final</version> </dependency> <!-- Validation API --> <dependency> <groupId>jakarta.validation</groupId> <artifactId>jakarta.validation-api</artifactId> <version>3.0.2</version> </dependency> <!-- Expression Language (for message interpolation) --> <dependency> <groupId>org.glassfish</groupId> <artifactId>jakarta.el</artifactId> <version>4.0.2</version> </dependency> </dependencies>

Basic Custom Constraint Structure

A custom constraint consists of three parts:

  1. Annotation: Defines the constraint with attributes
  2. Validator: Contains the actual validation logic
  3. Error Messages: Custom validation messages

Simple Custom Constraint Examples

Example 1: Basic Custom Annotation

import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.*; // Custom annotation for valid email domains @Documented @Constraint(validatedBy = ValidEmailDomainValidator.class) @Target({ ElementType.FIELD, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) public @interface ValidEmailDomain { // Default error message String message() default "Invalid email domain"; // Required by validation API Class<?>[] groups() default {}; // Required by validation API Class<? extends Payload>[] payload() default {}; // Custom attribute - allowed domains String[] allowedDomains() default { "company.com", "example.com" }; }

Example 2: Corresponding Validator

import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; public class ValidEmailDomainValidator implements ConstraintValidator<ValidEmailDomain, String> { private String[] allowedDomains; @Override public void initialize(ValidEmailDomain constraintAnnotation) { this.allowedDomains = constraintAnnotation.allowedDomains(); } @Override public boolean isValid(String email, ConstraintValidatorContext context) { // Null values are considered valid (use @NotNull for null checks) if (email == null) { return true; } // Check if email contains @ and has valid domain if (!email.contains("@")) { return false; } String domain = email.substring(email.indexOf("@") + 1); // Check against allowed domains for (String allowedDomain : allowedDomains) { if (domain.equalsIgnoreCase(allowedDomain)) { return true; } } return false; } }

Example 3: Using the Custom Constraint

public class User { @NotBlank(message = "Name is required") private String name; @ValidEmailDomain( allowedDomains = {"company.com", "partner.com"}, message = "Email must be from company or partner domain" ) private String email; // Constructors, getters, setters public User(String name, String email) { this.name = name; this.email = email; } 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; } }

Advanced Custom Constraints

Example 4: Cross-Field Validation

import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.*; // Annotation for validating that two fields match @Documented @Constraint(validatedBy = FieldMatchValidator.class) @Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface FieldMatch { String message() default "Fields must match"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; // First field String first(); // Second field String second(); // Allow multiple annotations of the same type @Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) @interface List { FieldMatch[] value(); } }

Example 5: Cross-Field Validator

import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.lang.reflect.Field; public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object> { private String firstFieldName; private String secondFieldName; @Override public void initialize(FieldMatch constraintAnnotation) { this.firstFieldName = constraintAnnotation.first(); this.secondFieldName = constraintAnnotation.second(); } @Override public boolean isValid(Object value, ConstraintValidatorContext context) { if (value == null) { return true; } try { // Use reflection to get field values Object firstValue = getFieldValue(value, firstFieldName); Object secondValue = getFieldValue(value, secondFieldName); // Check if both values are equal boolean isValid = (firstValue == null && secondValue == null) || (firstValue != null && firstValue.equals(secondValue)); // Customize error message if validation fails if (!isValid) { context.disableDefaultConstraintViolation(); context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()) .addPropertyNode(secondFieldName) .addConstraintViolation(); } return isValid; } catch (Exception e) { throw new RuntimeException("Error during field match validation", e); } } private Object getFieldValue(Object object, String fieldName) throws Exception { Field field = object.getClass().getDeclaredField(fieldName); field.setAccessible(true); return field.get(object); } }

Example 6: Using Cross-Field Validation

@FieldMatch.List({ @FieldMatch(first = "password", second = "confirmPassword", message = "Password fields must match"), @FieldMatch(first = "email", second = "confirmEmail", message = "Email fields must match") }) public class RegistrationForm { @NotBlank private String username; @Email private String email; @NotBlank private String confirmEmail; @Size(min = 8, message = "Password must be at least 8 characters") private String password; private String confirmPassword; // Constructors, getters, setters public RegistrationForm() {} public RegistrationForm(String username, String email, String confirmEmail, String password, String confirmPassword) { this.username = username; this.email = email; this.confirmEmail = confirmEmail; this.password = password; this.confirmPassword = confirmPassword; } // Getters and setters public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getConfirmEmail() { return confirmEmail; } public void setConfirmEmail(String confirmEmail) { this.confirmEmail = confirmEmail; } 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; } }

Complex Business Logic Constraints

Example 7: Age Validation Constraint

import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.*; @Documented @Constraint(validatedBy = AgeValidator.class) @Target({ ElementType.FIELD, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) public @interface ValidAge { String message() default "Invalid age"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; int min() default 0; int max() default 150; }

Example 8: Age Validator

import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.time.LocalDate; import java.time.Period; public class AgeValidator implements ConstraintValidator<ValidAge, LocalDate> { private int minAge; private int maxAge; @Override public void initialize(ValidAge constraintAnnotation) { this.minAge = constraintAnnotation.min(); this.maxAge = constraintAnnotation.max(); } @Override public boolean isValid(LocalDate birthDate, ConstraintValidatorContext context) { // Null values are considered valid if (birthDate == null) { return true; } // Calculate age LocalDate today = LocalDate.now(); Period period = Period.between(birthDate, today); int age = period.getYears(); // Validate age range boolean isValid = age >= minAge && age <= maxAge; // Custom error message if (!isValid) { context.disableDefaultConstraintViolation(); String message = String.format("Age must be between %d and %d years", minAge, maxAge); context.buildConstraintViolationWithTemplate(message).addConstraintViolation(); } return isValid; } }

Example 9: Conditional Validation Constraint

import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.*; @Documented @Constraint(validatedBy = ConditionalValidator.class) @Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface ConditionalValidation { String message() default "Conditional validation failed"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; // Field that determines the condition String conditionField(); // Expected value for the condition String conditionValue(); // Fields that are required when condition is met String[] requiredFields(); }

Example 10: Conditional Validator

import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.lang.reflect.Field; public class ConditionalValidator implements ConstraintValidator<ConditionalValidation, Object> { private String conditionField; private String conditionValue; private String[] requiredFields; @Override public void initialize(ConditionalValidation constraintAnnotation) { this.conditionField = constraintAnnotation.conditionField(); this.conditionValue = constraintAnnotation.conditionValue(); this.requiredFields = constraintAnnotation.requiredFields(); } @Override public boolean isValid(Object value, ConstraintValidatorContext context) { if (value == null) { return true; } try { // Check if condition is met Object conditionFieldValue = getFieldValue(value, conditionField); boolean conditionMet = conditionValue.equals(String.valueOf(conditionFieldValue)); if (!conditionMet) { return true; // Condition not met, no further validation needed } // Condition met, validate required fields boolean isValid = true; context.disableDefaultConstraintViolation(); for (String requiredField : requiredFields) { Object fieldValue = getFieldValue(value, requiredField); if (fieldValue == null || (fieldValue instanceof String && ((String) fieldValue).trim().isEmpty())) { isValid = false; context.buildConstraintViolationWithTemplate( "Field '" + requiredField + "' is required when " + conditionField + " is '" + conditionValue + "'") .addPropertyNode(requiredField) .addConstraintViolation(); } } return isValid; } catch (Exception e) { throw new RuntimeException("Error during conditional validation", e); } } private Object getFieldValue(Object object, String fieldName) throws Exception { Field field = object.getClass().getDeclaredField(fieldName); field.setAccessible(true); return field.get(object); } }

Using Custom Constraints

Example 11: Complete Usage Example

@ConditionalValidation( conditionField = "userType", conditionValue = "BUSINESS", requiredFields = {"companyName", "taxId"}, message = "Company name and tax ID are required for business users" ) public class CompleteUser { @NotBlank private String username; @ValidEmailDomain(allowedDomains = {"company.com"}) private String email; @ValidAge(min = 18, max = 100, message = "Must be between 18 and 100 years old") private LocalDate birthDate; private String userType; // PERSONAL or BUSINESS private String companyName; private String taxId; @FieldMatch(first = "password", second = "confirmPassword") private String password; private String confirmPassword; // Constructors public CompleteUser() {} public CompleteUser(String username, String email, LocalDate birthDate, String userType, String companyName, String taxId) { this.username = username; this.email = email; this.birthDate = birthDate; this.userType = userType; this.companyName = companyName; this.taxId = taxId; } // Getters and setters public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public LocalDate getBirthDate() { return birthDate; } public void setBirthDate(LocalDate birthDate) { this.birthDate = birthDate; } public String getUserType() { return userType; } public void setUserType(String userType) { this.userType = userType; } public String getCompanyName() { return companyName; } public void setCompanyName(String companyName) { this.companyName = companyName; } public String getTaxId() { return taxId; } public void setTaxId(String taxId) { this.taxId = taxId; } 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; } }

Testing Custom Constraints

Example 12: Unit Testing Validators

import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import javax.validation.Validation; import javax.validation.Validator; import javax.validation.ValidatorFactory; import java.time.LocalDate; import java.util.Set; import static org.junit.jupiter.api.Assertions.*; class CustomConstraintTest { private Validator validator; @BeforeEach void setUp() { ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); validator = factory.getValidator(); } @Test void testValidEmailDomain() { CompleteUser user = new CompleteUser(); user.setEmail("[email protected]"); user.setUsername("john"); user.setBirthDate(LocalDate.of(1990, 1, 1)); user.setUserType("PERSONAL"); Set<ConstraintViolation<CompleteUser>> violations = validator.validate(user); assertEquals(0, violations.size()); } @Test void testInvalidEmailDomain() { CompleteUser user = new CompleteUser(); user.setEmail("[email protected]"); // Invalid domain user.setUsername("john"); user.setBirthDate(LocalDate.of(1990, 1, 1)); user.setUserType("PERSONAL"); Set<ConstraintViolation<CompleteUser>> violations = validator.validate(user); assertEquals(1, violations.size()); assertTrue(violations.iterator().next().getMessage().contains("email domain")); } @Test void testConditionalValidation() { CompleteUser user = new CompleteUser(); user.setEmail("[email protected]"); user.setUsername("john"); user.setBirthDate(LocalDate.of(1990, 1, 1)); user.setUserType("BUSINESS"); // Business user but missing required fields Set<ConstraintViolation<CompleteUser>> violations = validator.validate(user); assertEquals(2, violations.size()); // companyName and taxId are required } @Test void testAgeValidation() { CompleteUser user = new CompleteUser(); user.setEmail("[email protected]"); user.setUsername("john"); user.setBirthDate(LocalDate.of(2010, 1, 1)); // Under 18 user.setUserType("PERSONAL"); Set<ConstraintViolation<CompleteUser>> violations = validator.validate(user); assertEquals(1, violations.size()); assertTrue(violations.iterator().next().getMessage().contains("between 18 and 100")); } @Test void testFieldMatchValidation() { CompleteUser user = new CompleteUser(); user.setEmail("[email protected]"); user.setUsername("john"); user.setBirthDate(LocalDate.of(1990, 1, 1)); user.setUserType("PERSONAL"); user.setPassword("password123"); user.setConfirmPassword("differentpassword"); // Doesn't match Set<ConstraintViolation<CompleteUser>> violations = validator.validate(user); assertEquals(1, violations.size()); assertTrue(violations.iterator().next().getMessage().contains("must match")); } }

Integration with Spring Boot

Example 13: Spring Boot Configuration

@Configuration public class ValidationConfig { @Bean public Validator validator() { return Validation.buildDefaultValidatorFactory().getValidator(); } @Bean public MethodValidationPostProcessor methodValidationPostProcessor() { return new MethodValidationPostProcessor(); } } // Service using validation @Service @Validated public class UserService { public void createUser(@Valid CompleteUser user) { // Business logic here System.out.println("Creating user: " + user.getUsername()); } public void updateEmail(@ValidEmailDomain String email) { // Method parameter validation System.out.println("Updating email: " + email); } } // REST Controller @RestController @RequestMapping("/api/users") public class UserController { @Autowired private UserService userService; @PostMapping public ResponseEntity<?> createUser(@RequestBody @Valid CompleteUser user) { userService.createUser(user); return ResponseEntity.ok("User created successfully"); } }

Best Practices

  1. Keep Validators Simple: Focus on single responsibility
  2. Use Composition: Combine multiple simple validators rather than creating complex ones
  3. Proper Error Messages: Provide meaningful, user-friendly error messages
  4. Test Thoroughly: Write comprehensive tests for your validators
  5. Consider Performance: Avoid expensive operations in validators
  6. Use Standard Constraints First: Leverage built-in constraints when possible
  7. Document Custom Constraints: Provide clear documentation for your custom constraints

Common Patterns

Example 14: Enum Validation

// Custom constraint for enum values @Documented @Constraint(validatedBy = EnumValidator.class) @Target({ ElementType.FIELD, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) public @interface ValidEnum { String message() default "Invalid enum value"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; Class<? extends Enum<?>> enumClass(); } // Enum validator public class EnumValidator implements ConstraintValidator<ValidEnum, String> { private Class<? extends Enum<?>> enumClass; private Set<String> validValues; @Override public void initialize(ValidEnum constraintAnnotation) { this.enumClass = constraintAnnotation.enumClass(); this.validValues = Arrays.stream(enumClass.getEnumConstants()) .map(Enum::name) .collect(Collectors.toSet()); } @Override public boolean isValid(String value, ConstraintValidatorContext context) { if (value == null) { return true; } return validValues.contains(value); } }

Conclusion

Hibernate Validator custom constraints provide powerful capabilities for:

Key Benefits:

  • Domain-specific validation: Tailor validation to your business rules
  • Reusable validation logic: Apply consistent rules across your application
  • Clean separation: Keep validation logic separate from business logic
  • Complex validation: Handle multi-field and conditional validation scenarios

Common Use Cases:

  • Cross-field validation (password confirmation, email confirmation)
  • Business rule validation (age restrictions, domain restrictions)
  • Conditional validation (required fields based on other field values)
  • Enum validation (restrict input to specific enum values)
  • Custom format validation (phone numbers, IDs, codes)

By creating custom constraints, you can build a robust validation framework that ensures data integrity and provides clear, user-friendly error messages throughout your application.


Remember: Always test your custom validators thoroughly and consider performance implications, especially for validators that use reflection or make external calls.

Leave a Reply

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


Macro Nepal Helper