I am trying to implement a custom annotation that is able to validate data fields across various DTO classes. Specifically, the validation rules are going to be based on regex strings which I intend to reuse across these (many, many) DTOs.
To add context, imagine I have a set of data fields like phoneNumber
, emailAddress
, ipAddress
, etc. which are essentially going to appear in many of the DTOs in different combinations. Each of these fields would then have its own regex validation.
I have researched on how to create these annotations and validator using the ConstraintValidator<A extends Annotation, T>
interface. However, I want my all my validation rules to be captured in a single JSON string which can be written to my configuration server (thus making it dynamic and easily reconfigured) and read via the @Value
annotation.
The key problem lies in that my ConstraintValidator class is not able to access this @Value object which is stored in a @Configuration class
. Let's take a look at the classes below.
Here is my custom annotation class:
@Target({ ElementType.FIELD })
@Retention( RetentionPolicy.RUNTIME )
@Constraint(validatedBy = DynamicRegexValidator.class)
public @interface RegexValidatedField {
String fieldName();
String message() default "Invalid field";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
And this is my ConstraintValidator implementation:
public class DynamicRegexValidator implements ConstraintValidator<RegexValidatedField, String> {
@Autowired
private ValidatorConfiguration validatorConfiguration;
private String fieldName;
@Override
public void initialize(RegexValidatedField constraintAnnotation) {
this.fieldName = constraintAnnotation.fieldName();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
ValidationRule rule = validatorConfiguration.getRuleForField(fieldName);
if (rule == null || value == null) {
return true;
}
if (!value.matches(rule.getRegex())) {
return false;
}
return true;
}
}
Lastly, the validator configuration class that is being injected into the ConstraintValidator:
@Configuration
public class ValidatorConfiguration {
@Value("${validation.rules:}")
private String validationRulesJson;
private Map<String, ValidationRule> validationRules;
@PostConstruct
public void init() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
validationRules = objectMapper.readValue(validationRulesJson, new TypeReference<Map<String, ValidationRule>>() {});
}
public ValidationRule getRuleForField(String fieldName) {
return validationRules.get(fieldName);
}
}
You can imagine that the @Value
string that defines my validation rules is just going to be a key-value pair of data field names and its corresponding regex string. As I run my application in DEBUG mode with different breakpoints, I can see that the error occurs in the isValid()
method because validatorConfiguration
is always null.
After some reading, my limited understanding on this is that ConstraintValidator classes are managed by Hibernate and not Spring, so it is not able to access Spring-managed beans (my ValidatorConfiguration
class).
I have tried some of the solutions posted on these two other questions, but was not able to fix it:
Autowired gives Null value in Custom Constraint validator
@Autowired in ConstraintValidator inject Null
Perhaps I am not applying the solution correctly, but I'm hoping someone can point me in the right direction to solving this, or whether there are other methods of achieving what I am trying to do here.
P.S org.hibernate.validator is on version 6.2.5, and I am running Spring 5.3.25.
After further research and deep diving into my code, I have discovered my mistake. I hope this answer helps to provide additional context and can help others who may potentially encounter the same pitfall as I have.
The mistake I made was not fully understanding how custom validators are instantiated and managed.
Previously, I have already written a separate utility class that performs validation using a generic method like
public <T> Map<String, List<String>> validate(T object)
In this utility class, I have instantiated a static Validator instance like so:
private static Validator validator = Validation.buildDefaultValidatorFactory().getValidator()
Therein lies the root cause of the null pointer problem.
By using the default validator factory, we are bypassing Spring's dependency injection mechanism because the method creates a new validator using the default JSR-303 Bean Validation factory (typically a Hibernate Validator) without knowledge of the Spring context.
Consequently, custom ConstraintValidator
instances are not created by Spring and hence any Spring-specific features like Autowired
or @Value
will not work because Spring is not managing those custom validators.
After researching further, the only addition that needs to be made is as follows:
@Configuration
public class ValidatorConfig {
@Bean
public Validator validator() {
return new LocalValidatorFactoryBean();
}
}
The solution in my situation is to do the following:
LocalValidatorFactoryBean
by setting up the configuration class as shown above.Validator
using buildDefaultValidatorFactory()
and inject the Validator
bean that is actually managed by Spring.This way, Spring will manage the lifecycle of DynamicRegexValidator
and other Spring managed service and configuration classes will then be accessible and thus injected into the ConstraintValidator
implementations.
This solution was detailed here as well: Injecting Spring Dependencies into ConstrantValidator