Spring Data's validation framework provides robust built-in validators, but sometimes we need custom validation logic for specific business rules. In this article, I'll show you how to create and implement custom validators in Spring Data.
Understanding Custom Validation
Custom validators in Spring allow us to define specific validation rules that aren't covered by standard annotations like @NotNull
or @Size
. They're particularly useful when dealing with complex business logic or domain-specific validation requirements.
Creating a Custom Validator
Let's create a custom validator that checks if a string follows a specific business format. Here's a step-by-step example:
- First, create a custom annotation:
@Documented
@Constraint(validatedBy = BusinessCodeValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidBusinessCode {
String message() default "Invalid business code format";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
- Implement the validator class:
public class BusinessCodeValidator implements ConstraintValidator<ValidBusinessCode, String> {
@Override
public void initialize(ValidBusinessCode constraintAnnotation) {
// Initialization logic if needed
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return true; // Let @NotNull handle null checking
}
// Custom validation logic
return value.matches("^BC-[0-9]{4}-[A-Z]{2}$");
}
}
- Apply the validator to your entity:
@Entity
public class Business {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ValidBusinessCode
private String businessCode;
// getters and setters
}
Advanced Validation Features
Composite Validators
Sometimes you need to combine multiple validation rules. Here's how to create a composite validator:
@Documented
@Constraint(validatedBy = CompositeValidator.class)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidBusinessEntity {
String message() default "Business validation failed";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class CompositeValidator implements ConstraintValidator<ValidBusinessEntity, Business> {
@Override
public boolean isValid(Business business, ConstraintValidatorContext context) {
boolean isValid = true;
if (!isValidBusinessCode(business.getBusinessCode())) {
context.buildConstraintViolationWithTemplate("Invalid business code")
.addPropertyNode("businessCode")
.addConstraintViolation();
isValid = false;
}
if (!isValidDateRange(business.getStartDate(), business.getEndDate())) {
context.buildConstraintViolationWithTemplate("Invalid date range")
.addPropertyNode("dateRange")
.addConstraintViolation();
isValid = false;
}
return isValid;
}
}
Cross-Field Validation
For validations that involve multiple fields:
@ValidDateRange
public class DateRange {
private LocalDate startDate;
private LocalDate endDate;
// getters and setters
}
public class DateRangeValidator implements ConstraintValidator<ValidDateRange, DateRange> {
@Override
public boolean isValid(DateRange range, ConstraintValidatorContext context) {
if (range.getStartDate() == null || range.getEndDate() == null) {
return true;
}
return !range.getStartDate().isAfter(range.getEndDate());
}
}
Best Practices
Separation of Concerns: Keep validation logic isolated in dedicated validator classes.
Meaningful Messages: Provide clear, user-friendly validation messages:
@ValidBusinessCode(message = "Business code must follow format: BC-XXXX-YY")
private String businessCode;
- Null Handling: Be explicit about null handling in your validators:
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return true; // Or false, depending on your requirements
}
// validation logic
}
- Context-Specific Validation: Use validation groups for different validation contexts:
public interface CreateValidation {}
public interface UpdateValidation {}
@ValidBusinessCode(groups = {CreateValidation.class})
private String businessCode;
Testing Custom Validators
Don't forget to test your validators:
@Test
public void testBusinessCodeValidator() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Business business = new Business();
business.setBusinessCode("invalid-code");
Set<ConstraintViolation<Business>> violations = validator.validate(business);
assertFalse(violations.isEmpty());
assertEquals("Invalid business code format",
violations.iterator().next().getMessage());
}
Error Handling
Implement a global exception handler to manage validation errors:
@ControllerAdvice
public class ValidationExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, List<String>>> handleValidationErrors(
MethodArgumentNotValidException ex) {
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.toList());
return new ResponseEntity<>(getErrorsMap(errors), HttpStatus.BAD_REQUEST);
}
private Map<String, List<String>> getErrorsMap(List<String> errors) {
Map<String, List<String>> errorResponse = new HashMap<>();
errorResponse.put("errors", errors);
return errorResponse;
}
}
Conclusion
Custom validators in Spring Data provide a powerful way to implement complex validation rules. By following these patterns and best practices, you can create maintainable, reusable validation components that enhance your application's data integrity.
Remember to keep your validators focused, well-tested, and documented. This will make them easier to maintain and reuse across your application.
The examples provided here should give you a solid foundation for implementing your own custom validators in Spring Data projects. Happy coding!
Top comments (0)