Search code examples
javaspringvalidationhibernate-validator

ConstraintValidator @Autowired injects null


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.


Solution

  • 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:

    1. Configure LocalValidatorFactoryBean by setting up the configuration class as shown above.
    2. Remove the creation of 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