1

Suppose I don't wanna use JSR 303 to validate my beans. Is it possible to implement custom validator which will be used by Spring when bean is marked as @Valid?

It would be great if my custom validator will be a Spring Component

fedor.belov
  • 22,343
  • 26
  • 89
  • 134

2 Answers2

1

@fedor.belov

I detailed this problem in another question. In my needs I want to keep JSR-303 and make custom validator works, but you can change my example code to fit your needs.

This problem can be solved extending the LocalValidatorFactoryBean, you can override the validate method inside this class giving any behavior that you want.

In my case I need to use JSR-303 AND custom validators for same model in different methods in same Controller, normally is recommended to use @InitBinder, but it is not sufficient for my case because InitBinder make a bind between Model and Validator (if you use @RequestBody InitBinder is just for one model and one validator per Controller).

Controller

@RestController
public class LoginController {

    @PostMapping("/test")
    public Test test(@Validated(TestValidator.class) @RequestBody Test test) {
        return test;
    }

    @PostMapping("/test2")
    public Test test2(@Validated @RequestBody Test test) {
        return test;
    }
}

Custom Validator

public class TestValidator implements org.springframework.validation.Validator {

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

    @Override
    public void validate(Object target, Errors errors) {
        Test test = (Test) target;
        errors.rejectValue("field3", "weird");
        System.out.println(test.getField1());
        System.out.println(test.getField2());
        System.out.println(test.getField3());
     }
}

Class to be validate

public class Test {

    @Size(min = 3)
    private String field2;

    @NotNull
    @NotEmpty
    private String field1;

    @NotNull
    @Past
    private LocalDateTime field3;

    //...
    //getter/setter
    //...
}

CustomLocalValidatorFactoryBean

public class CustomLocalValidatorFactoryBean extends LocalValidatorFactoryBean {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public void validate(@Nullable Object target, Errors errors, @Nullable Object... validationHints) {
        Set<Validator> concreteValidators = new LinkedHashSet<>();
        Set<Class<?>> interfaceGroups = new LinkedHashSet<>();
        extractConcreteValidatorsAndInterfaceGroups(concreteValidators, interfaceGroups, validationHints);
        proccessConcreteValidators(target, errors, concreteValidators);
        processConstraintViolations(super.validate(target, interfaceGroups.toArray(new Class<?>[interfaceGroups.size()])), errors);
    }

    private void proccessConcreteValidators(Object target, Errors errors, Set<Validator> concreteValidators) {
        for (Validator validator : concreteValidators) {
            validator.validate(target, errors);
        }
    }

    private void extractConcreteValidatorsAndInterfaceGroups(Set<Validator> concreteValidators, Set<Class<?>> groups, Object... validationHints) {
        if (validationHints != null) {
            for (Object hint : validationHints) {
                if (hint instanceof Class) {
                    if (((Class<?>) hint).isInterface()) {
                        groups.add((Class<?>) hint);
                    } else {
                        Optional<Validator> validatorOptional = getValidatorFromGenericClass(hint);
                        if (validatorOptional.isPresent()) {
                            concreteValidators.add(validatorOptional.get());
                        }
                    }
                }
            }
        }
    }

    @SuppressWarnings("unchecked")
    private Optional<Validator> getValidatorFromGenericClass(Object hint) {
        try {
            Class<Validator> clazz = (Class<Validator>) Class.forName(((Class<?>) hint).getName());
            return Optional.of(clazz.newInstance());
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
            logger.info("There is a problem with the class that you passed to "
                    + " @Validated annotation in the controller, we tried to "
                    + " cast to org.springframework.validation.Validator and we cant do this");
        }
        return Optional.empty();
    }

}

Configure application

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean
    public javax.validation.Validator localValidatorFactoryBean() {
        return new CustomLocalValidatorFactoryBean();
    }
}

Input to /test endpoint:

{
    "field1": "",
    "field2": "aaaa",
    "field3": "2018-04-15T15:10:24"
}

Output from /test endpoint:

{
    "timestamp": "2018-04-16T17:34:28.532+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "weird.test.field3",
                "weird.field3",
                "weird.java.time.LocalDateTime",
                "weird"
            ],
            "arguments": null,
            "defaultMessage": null,
            "objectName": "test",
            "field": "field3",
            "rejectedValue": "2018-04-15T15:10:24",
            "bindingFailure": false,
            "code": "weird"
        },
        {
            "codes": [
                "NotEmpty.test.field1",
                "NotEmpty.field1",
                "NotEmpty.java.lang.String",
                "NotEmpty"
            ],
            "arguments": [
                {
                    "codes": [
                        "test.field1",
                        "field1"
                    ],
                    "arguments": null,
                    "defaultMessage": "field1",
                    "code": "field1"
                }
            ],
            "defaultMessage": "Não pode estar vazio",
            "objectName": "test",
            "field": "field1",
            "rejectedValue": "",
            "bindingFailure": false,
            "code": "NotEmpty"
        }
    ],
    "message": "Validation failed for object='test'. Error count: 2",
    "path": "/user/test"
}

Input to /test2 endpoint:

{
    "field1": "",
    "field2": "aaaa",
    "field3": "2018-04-15T15:10:24"
}

Output to /test2 endpoint:

{
    "timestamp": "2018-04-16T17:37:30.889+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "NotEmpty.test.field1",
                "NotEmpty.field1",
                "NotEmpty.java.lang.String",
                "NotEmpty"
            ],
            "arguments": [
                {
                    "codes": [
                        "test.field1",
                        "field1"
                    ],
                    "arguments": null,
                    "defaultMessage": "field1",
                    "code": "field1"
                }
            ],
            "defaultMessage": "Não pode estar vazio",
            "objectName": "test",
            "field": "field1",
            "rejectedValue": "",
            "bindingFailure": false,
            "code": "NotEmpty"
        }
    ],
    "message": "Validation failed for object='test'. Error count: 1",
    "path": "/user/test2"
}

Original answer with original question.

I hope this help.

Marco Blos
  • 933
  • 9
  • 22
  • I'm getting working while calling this endpoint: `There is a problem with the class that you passed to @Validated annotation in the controller, we tried to cast to org.springframework.validation.Validator and we cant do this` – Pranay Kumbhalkar Dec 06 '18 at 10:11
  • Hi @PranayKumbhalkar, do you trying to cast `TestValidator.class` to org.springframework.validation.Validator? Why do you trying to make it? – Marco Blos Dec 12 '18 at 13:39
  • 1
    Works great, thank you. Personally I just use the class *CustomLocalValidatorFactoryBean* and override *processConstraintViolations* to throw a custom exception based on the violations (allows to populate the response with a business error code, concatenate error messages if there are several, etc). Also, you can just annotate the class with *@Configuration* instead of declaring it in the root class *Application*. – Flyout91 Feb 06 '20 at 10:18
0

You will have to implement Validator interface and mark the implementation as @Component. Then you can bind your custom validator using @InitBinder annotation. Something like this:

@Component
public class MyCustomValidator implements Validator
{

    @Override
    public boolean supports(Class<?> clazz)
    {
        // your custom logic
    }

    @Override
    public void validate(Object target, Errors errors)
    {
        // your custom logic
    }
}

To bind it with annotation:

@Controller
public class MyController
{
    @Autowired
    private MyCustomValidator validator;

    @InitBinder
    protected void initBinder(final WebDataBinder binder)
    {
        binder.addValidators(validator);
    }
}
articuno
  • 546
  • 5
  • 12
  • 1
    This approach requires a lot of boilerplate code in a controller. Final implementation should require only @Valid annotation (or any other custom one) – fedor.belov Jul 14 '17 at 22:59