DEV Community

Parzival
Parzival

Posted on • Edited on

Custom Validators in Spring Data

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:

  1. 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 {};
}
Enter fullscreen mode Exit fullscreen mode
  1. 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}$");
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. 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
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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());
    }
}
Enter fullscreen mode Exit fullscreen mode

Best Practices

  1. Separation of Concerns: Keep validation logic isolated in dedicated validator classes.

  2. Meaningful Messages: Provide clear, user-friendly validation messages:

@ValidBusinessCode(message = "Business code must follow format: BC-XXXX-YY")
private String businessCode;
Enter fullscreen mode Exit fullscreen mode
  1. 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
}
Enter fullscreen mode Exit fullscreen mode
  1. Context-Specific Validation: Use validation groups for different validation contexts:
public interface CreateValidation {}
public interface UpdateValidation {}

@ValidBusinessCode(groups = {CreateValidation.class})
private String businessCode;
Enter fullscreen mode Exit fullscreen mode

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());
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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!

Heroku

Deliver your unique apps, your own way.

Heroku tackles the toil — patching and upgrading, 24/7 ops and security, build systems, failovers, and more. Stay focused on building great data-driven applications.

Learn More

Top comments (0)

Tiugo image

Fast, Lean, and Fully Extensible

CKEditor 5 is built for developers who value flexibility and speed. Pick the features that matter, drop the ones that don’t and enjoy a high-performance WYSIWYG that fits into your workflow

Start now

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, cherished by the supportive DEV Community. Coders of every background are encouraged to bring their perspectives and bolster our collective wisdom.

A sincere “thank you” often brightens someone’s day—share yours in the comments below!

On DEV, the act of sharing knowledge eases our journey and forges stronger community ties. Found value in this? A quick thank-you to the author can make a world of difference.

Okay