Search code examples
javaspringvalidationspring-bootbean-validation

Implementing custom validation logic for a Spring Boot endpoint using a combination of JSR-303 and Spring's Validator


I'm trying to implement some custom validation logic for a Spring Boot endpoint using a combination of JSR-303 Bean Validation API and Spring's Validator.

Based on the Validator class diagram it appears to be possible to extend one of CustomValidatorBean, SpringValidatorAdapter or LocalValidatorFactoryBean to add some custom validation logic into an overridden method validate(Object target, Errors errors).

Validator class diagram.

However, if I create a validator extending any of these three classes and register it using @InitBinder its validate(Object target, Errors errors) method is never invoked and no validation is performed. If I remove @InitBinder then a default Spring validator performs the JSR-303 Bean Validation.

REST controller:

@RestController
public class PersonEndpoint {

    @InitBinder("person")
    protected void initBinder(WebDataBinder binder) {
        binder.setValidator(new PersonValidator());
    }

    @RequestMapping(path = "/person", method = RequestMethod.PUT)
    public ResponseEntity<Person> add(@Valid @RequestBody Person person) {
        
        person = personService.save(person);
        return ResponseEntity.ok().body(person);
    }
}

Custom validator:

public class PersonValidator extends CustomValidatorBean {

    @Override
    public boolean supports(Class<?> clazz) {
        return Person.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        super.validate(target, errors);
        System.out.println("PersonValidator.validate() target="+ target +" errors="+ errors);
    }

}

If my validator implements org.springframework.validation.Validator then its validate(Object target, Errors errors) method is called but JSR-303 Bean Validation is not performed prior to it. I can implement my custom JSR-303 validation similar to the way SpringValidatorAdapter implements its JSR-303 Bean Validation, but there has to be a way to extend it instead:

    @Override
    public void validate(Object target, Errors errors) {
        if (this.targetValidator != null) {
            processConstraintViolations(this.targetValidator.validate(target), errors);
        }
    }

I have looked at using custom JSR-303 constraints to avoid using org.springframework.validation.Validator all together but there must be a way to make a custom validator work.

Spring validation documentation is not super clear on combining the two:

An application can also register additional Spring Validator instances per DataBinder instance, as described in Section 9.8.3, “Configuring a DataBinder”. This may be useful for plugging in validation logic without the use of annotations.

And then later on it touches on configuring multiple Validator instances:

A DataBinder can also be configured with multiple Validator instances via dataBinder.addValidators and dataBinder.replaceValidators. This is useful when combining globally configured Bean Validation with a Spring Validator configured locally on a DataBinder instance. See ???.

I'm using Spring Boot version 1.4.0.


Solution

  • Per @M.Deinum - using addValidators() instead of setValidator() did the trick. I also agree that using JSR-303, @AssertTrue method-based annotation specifically for cross fields validation, is probably a cleaner solution. A code example is available at https://github.com/pavelfomin/spring-boot-rest-example/tree/feature/custom-validator. In the example, the middle name validation is performed via custom spring validator while last name validation is handled by the default jsr 303 validator.