Convert JSR-303 validation errors to Spring's BindingResult
Asked Answered
T

5

19

I have the following code in a Spring controller:

@Autowired
private javax.validation.Validator validator;

@RequestMapping(value = "/submit", method = RequestMethod.POST)
public String submitForm(CustomForm form) {
    Set<ConstraintViolation<CustomForm>> errors = validator.validate(form);
    ...
}

Is it possible to map errors to Spring's BindingResult object without manually going through all the errors and adding them to the BindingResult? Something like this:

// NOTE: this is imaginary code
BindingResult bindingResult = BindingResult.fromConstraintViolations(errors);

I know it is possible to annotate the CustomForm parameter with @Valid and let Spring inject BindingResult as another method's parameter, but it's not an option in my case.

// I know this is possible, but doesn't work for me
public String submitForm(@Valid CustomForm form, BindingResult bindingResult) {
    ...
}
Tabithatablature answered 15/2, 2013 at 10:57 Comment(0)
W
11

A simpler approach could be to use Spring's abstraction org.springframework.validation.Validator instead, you can get hold of a validator by having this bean in the context:

<bean id="jsr303Validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" />

@Autowired @Qualifier("jsr303Validator") Validator validator;

With this abstraction in place, you can use the validator this way, passing in your bindingResult:

validator.validate(obj, bindingResult);
Wynd answered 15/2, 2013 at 11:19 Comment(3)
Thanks! That's definitely more concise.Tabithatablature
I don't see method validate which accepts BindingResultMaravedi
If you don't see the validate method that accepts the binding results, it means your Validator validator is a javax.validation.Validator. Change it to a org.springframework.validation.Validator and you'll be fine. No need to change the bean definition.Obsolesce
F
5

Spring uses a SpringValidatorAdapter to convert javax.validation.ConstraintViolation objects to ObjectError or FieldError objects, as found in the binding result. The BindStatus then uses a message source (like the web application context itself) to translate the errors. In short, you could do:

SpringValidatorAdapter springValidator = new SpringValidatorAdapter(validator);
BindingResult bindingResult= new BeanPropertyBindingResult(myBeanToValidate, "myBeanName");
springValidator.validate(myBeanToValidate, bindingResult);

This is easier when writing a unit test, because you don't even need to create a Spring context.

Fluidize answered 14/6, 2017 at 14:26 Comment(0)
G
1

Expanding on Kristiaan's answer, for testing purposes it is not necessary to create a spring context to validate using Spring's bindingResult. The following is an example:

public class ValidatorTest {

    javax.validation.Validator javaxValidator = Validation.buildDefaultValidatorFactory().getValidator();
    org.springframework.validation.Validator springValidator = new SpringValidatorAdapter(javaxValidator);

    @Test
    public void anExampleTest() {

    JSR303AnnotatedClassToTest   ctt  = new JSR303AnnotatedClassToTest( ..init vars..)

    ... test setup...

    WebDataBinder dataBinder = new WebDataBinder(ctt);
    dataBinder.setValidator(springValidator);
    dataBinder.validate();
    BindingResult bindingResult = dataBinder.getBindingResult(); 

    ... test analysis ...

    }
}

This approach doesn't require creating a binding result ahead of time, the dataBinder builds the right one for you.

Generalissimo answered 16/12, 2018 at 21:28 Comment(0)
A
1
@RequestMapping(value = "/submit", method = RequestMethod.POST)
public String submitForm(CustomForm form) {
    Set<ConstraintViolation<CustomForm>> errors = validator.validate(form);

    BindingResult bindingResult = toBindingResult(errors, form, "form");
    ...
}

private BindingResult toBindingResult(ConstraintViolationException e, Object object, String objectName) {
    BindingResult bindingResult = new BeanPropertyBindingResult(object, objectName);
    new AddConstraintViolationsToErrors().addConstraintViolations(e.getConstraintViolations(), bindingResult);
    return bindingResult;
}

private static class AddConstraintViolationsToErrors extends SpringValidatorAdapter {
    public AddConstraintViolationsToErrors() {
        super(Validation.buildDefaultValidatorFactory().getValidator()); // Validator is not actually used
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    public void addConstraintViolations(Set<? super ConstraintViolation<?>> violations, Errors errors) {
        // Using raw type since processConstraintViolations specifically expects ConstraintViolation<Object>
        super.processConstraintViolations((Set) violations, errors);
    }
}

Unlike the other answers to this question, this solution handles the case where there already exists a Set<ConstraintViolation<?>> which needs to be converted to to a BindingResult.

Explanation

Spring provides the SpringValidatorAdapter class to perform bean validations, storing the results in an Errors instance (note that BindingResult extends Errors). The normal manual use of this class would be to use it to perform the validations via the validate method:

Validator beanValidator = Validation.buildDefaultValidatorFactory().getValidator();
SpringValidatorAdapter validatorAdapter = new SpringValidatorAdapter(beanValidator);

BindException bindException = new BindException(form, "form");
validatorAdapter.validate(form, bindException);

However, this doesn't help in the case where there already exists a Set<ConstraintViolation<?>> which needs to be converted to a BindingResult.

It is still possible to achieve this goal, though it does require jumping through a couple extra hoops. SpringValidatorAdapter contains a processConstraintViolations method which converts the ConstraintViolation objects into the appropriate Spring ObjectError subtypes, and stores them on an Errors object. However, this method is protected, limiting its accesibility to subclasses.

This limitation can be worked around by creating a custom subclass of SpringValidatorAdapter which delegates to or exposes the protected method. It is not a typical usage, but it works.

public class AddConstraintViolationsToErrors extends SpringValidatorAdapter {
    public AddConstraintViolationsToErrors() {
        super(Validation.buildDefaultValidatorFactory().getValidator()); // Validator is not actually used
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    public void addConstraintViolations(Set<? super ConstraintViolation<?>> violations, Errors errors) {
        // Using raw type since processConstraintViolations specifically expects ConstraintViolation<Object>
        super.processConstraintViolations((Set) violations, errors);
    }
}

This custom class can be used to populate a newly created BindingResult, achieving the goal of creating a BindingResult from a Set<ConstraintViolation<?>>.

private BindingResult toBindException(ConstraintViolationException e, Object object, String objectName) {
    BindingResult bindingResult = new BeanPropertyBindingResult(object, objectName);
    new AddConstraintViolationsToErrors().addConstraintViolations(e.getConstraintViolations(), bindingResult);
    return bindingResult;
}
Apiarian answered 5/2, 2021 at 20:8 Comment(0)
S
0

I've encountered a similar issue and this is how I resolved it.

Given your example, this is how I implemented it

First, I used a smart validator, and in the method I let spring inject the BindingResult

@Autowired
private org.springframework.validation.SmartValidator validator;

@RequestMapping(value = "/submit", method = RequestMethod.POST)
public String submitForm(CustomForm form, BindingResult bindingResult) {
    Set<ConstraintViolation<CustomForm>> errors = validator.validate(form);
    ...
}

And then using that binding result i pass it in the SmartValidator so that any errors will be bounded to BindingResult.

validator.validate(form, bindingResult);
if(bindingResult.hasErrors()) {
     throw new BindException(bindingResult);
}
Subdeacon answered 22/9, 2021 at 4:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.