Adding custom ConstraintValidator for @Future and LocalDate to a Spring Boot project

I'd agree with Miloš that using the META-INF/validation.xml is probably the cleanest and easiest way, but if you do really want to set it up in a Spring @Confguration class then it is possible and here's one way you can do it.

The beauty of Spring Boot is that does a lot of configuration on your behalf so you don't have to worry about it. However, this can also cause problems when you want to specifically configure something yourself and it's not terribly obvious how to do it.

So yesterday I set about trying to add a CustomerValidator for @Past and LocalDate using the ConstraintDefinitionContributor mechanism that Hardy suggests (and is referred to in the Hibernate documentation).

The simple bit was to write the implementing class to do the validation, which for my highly specific purposes consisted of:

public class PastValidator implements ConstraintValidator<Past, LocalDate> {

    @Override
    public void initialize(Past constraintAnnotation) {}

    @Override
    public boolean isValid(LocalDate value, ConstraintValidatorContext context) {
        return null != value && value.isBefore(LocalDate.now());
    }
}

Then I got lazy and just instantiated a @Bean in my configuration, just on the off-chance that one of Spring's auto-configuration classes would just pick it up and wire it into the Hibernate validator. This was quite a long shot, given the available documentation (or lack thereof) and what Hardy and others had said, and it didn't pay off.

So I fired up a debugger and worked backwards from the exception being thrown in org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree which was telling me that it couldn't find a validator for @Past and LocalDate.

Looking at the type hierarchy of the ConstraintValidatorFactory I discovered that there were two implementing classes in my Spring MVC application: SpringConstraintValidatorFactory and SpringWebConstraintValidatorFactory which both just try and get a bean from the context of the correct class. This told me that I do have to have my validator registered with Spring's BeanFactory, however when I stuck a breakpoint on this but it didn't get hit for my PastValidator, which meant that Hibernate wasn't aware that it should be even requesting this class.

This made sense: there wasn't any ConstraintDefinitionContributor anywhere to do tell Hibernate it needed to ask Spring for an instance of the PastValidator. The example in the documentation at http://docs.jboss.org/hibernate/validator/5.2/reference/en-US/html_single/#section-constraint-definition-contributor suggests that I'd need access to a HibernateValidatorConfiguration so I just needed to find where Spring was doing its configuring.

After a little bit of digging I found that it was all happening in Spring's LocalValidatorFactoryBean class, specifically in its afterPropertiesSet() method. From its javadoc:

/*
 * This is the central class for {@code javax.validation} (JSR-303) setup in a Spring
 * application context: It bootstraps a {@code javax.validation.ValidationFactory} and
 * exposes it through the Spring {@link org.springframework.validation.Validator} interface
 * as well as through the JSR-303 {@link javax.validation.Validator} interface and the
 * {@link javax.validation.ValidatorFactory} interface itself.
 */

Basically, if you don't set up and configure your own Validator then this is where Spring tries to do it for you, and in true Spring style it provides a handy extension method so that you can let it do its configuration and then add your own into the mix.

So my solution was to just extend the LocalValidatorFactoryBean so that I'd be able to register my own ConstraintDefinitionContributor instances:

import java.util.ArrayList;
import java.util.List;
import javax.validation.Configuration;
import org.hibernate.validator.internal.engine.ConfigurationImpl;
import org.hibernate.validator.spi.constraintdefinition.ConstraintDefinitionContributor;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

public class ConstraintContributingValidatorFactoryBean extends LocalValidatorFactoryBean {

    private List<ConstraintDefinitionContributor> contributors = new ArrayList<>();

    public void addConstraintDefinitionContributor(ConstraintDefinitionContributor contributor) {
        contributors.add(contributor);
    }

    @Override
    protected void postProcessConfiguration(Configuration<?> configuration) {
        if(configuration instanceof ConfigurationImpl) {
            ConfigurationImpl config = ConfigurationImpl.class.cast(configuration);
            for(ConstraintDefinitionContributor contributor : contributors)
                config.addConstraintDefinitionContributor(contributor);
        }
    }
}

and then instantiate and configure this in my Spring config:

    @Bean
    public ConstraintContributingValidatorFactoryBean validatorFactory() {
        ConstraintContributingValidatorFactoryBean validatorFactory = new ConstraintContributingValidatorFactoryBean();
        validatorFactory.addConstraintDefinitionContributor(new ConstraintDefinitionContributor() {
            @Override
            public void collectConstraintDefinitions(ConstraintDefinitionBuilder builder) {
                    builder.constraint( Past.class )
                            .includeExistingValidators( true )
                            .validatedBy( PastValidator.class );
            }
        });
        return validatorFactory;
    }

and for completeness, here's also where I'd instantiated by PastValidator bean:

    @Bean
    public PastValidator pastValidator() {
        return new PastValidator();
    }

Other springy-thingies

I noticed in my debugging that because I've got quite a large Spring MVC application, I was seeing two instances of SpringConstraintValidatorFactory and one of SpringWebConstraintValidatorFactory. I found the latter was never used during validation so I just ignored it for the time being.

Spring also has a mechanism for deciding which implementation of ValidatorFactory to use, so it's possible for it to not use your ConstraintContributingValidatorFactoryBean and instead use something else (sorry, I found the class in which it did this yesterday but couldn't find it again today although I only spent about 2 minutes looking). If you're using Spring MVC in any kind of non-trivial way then chances are you've already had to write your own configuration class such as this one which implements WebMvcConfigurer where you can explicitly wire in your Validator bean:

public static class MvcConfigurer implements WebMvcConfigurer {

    @Autowired
    private ConstraintContributingValidatorFactoryBean validatorFactory;

    @Override
    public Validator getValidator() {
        return validatorFactory;
    }

    // ...
    // <snip>lots of other overridden methods</snip>
    // ...
}

This is wrong

As has been pointed out, you should be wary of applying a @Past validation to a LocalDate because there's no time zone information. However, if you're using LocalDate because everything will just run in the same time zone, or you deliberately want to ignore time zones, or you just don't care, then this is fine for you.


You'll need to add your own validator to the META-INF/validation.xml file, like so:

<constraint-mappings
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://jboss.org/xml/ns/javax/validation/mapping validation-mapping-1.1.xsd"
    xmlns="http://jboss.org/xml/ns/javax/validation/mapping" version="1.1">

    <constraint-definition annotation="javax.validation.constraints.Future">
        <validated-by include-existing-validators="true">
            <value>package.to.LocalDateFutureValidator</value>
        </validated-by>
    </constraint-definition>
</constraint-mappings>

For more details, refer to the official documentation.


In case someone has a problem with the validation.xml approach and is getting Cannot find the declaration of element constraint-mappings error, as I did, I had to make the following modifications. Hope this will save somebody the time I spent to figure this out.

META-INF/validation.xml:

<validation-config
        xmlns="http://jboss.org/xml/ns/javax/validation/configuration"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="
                    http://jboss.org/xml/ns/javax/validation/configuration
                    validation-configuration-1.1.xsd"
        version="1.1">
    <constraint-mapping>META-INF/validation/past.xml</constraint-mapping>
</validation-config>

META-INF/validation/past.xml:

<constraint-mappings
        xmlns="http://jboss.org/xml/ns/javax/validation/mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="
http://jboss.org/xml/ns/javax/validation/mapping
validation-mapping-1.1.xsd"
        version="1.1">
    <constraint-definition annotation="javax.validation.constraints.Past">
        <validated-by include-existing-validators="false">
            <value>your.package.PastConstraintValidator</value>
        </validated-by>
    </constraint-definition>
</constraint-mappings>