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
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
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.
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);
}
}