I am building a Spring Boot application and trying to implement custom validation for some DTOs/Entities that I will be validating in the service layer. Based on the Spring documentation on this matter, I think one way to do this is to implement the org.springframework.validation.Validator
interface.
As a minimal, complete, reproducible example, consider the following code:
Spring Initializr Bootstrapped Project
With the following code added in src/main/java/com.example.usingvalidation
:
// Person.java
package com.example.usingvalidation;
public class Person {
private String firstName;
private String lastName;
private int age;
private String gender;
public Person() {
}
public Person(String firstName, String lastName, int age, String gender) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.gender = gender;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
@Override
public String toString() {
return "Person{" +
"firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", age=" + age +
", gender='" + gender + '\'' +
'}';
}
}
// PersonValidator.java
package com.example.usingvalidation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
@Component
public class PersonValidator implements Validator {
private final Logger log = LoggerFactory.getLogger(this.getClass());
@Override
public boolean supports(Class<?> clazz) {
log.info("supports called");
return Person.class.equals(clazz);
}
@Override
public void validate(Object target, Errors errors) {
log.info("validate called");
Person person = (Person) target;
errors.reject("E00001", "This is the default error message, just to test.");
}
}
// MyController.java
package com.example.usingvalidation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.ConstraintViolation;
import java.util.Set;
@RestController
public class MyController {
private final Logger log = LoggerFactory.getLogger(this.getClass());
private final LocalValidatorFactoryBean validatorFactory;
@Autowired
public MyController(LocalValidatorFactoryBean validatorFactory) {
this.validatorFactory = validatorFactory;
}
@GetMapping("/")
public Person getPerson(@RequestBody Person person) {
log.info("calling validate");
Set<ConstraintViolation<Person>> errors = validatorFactory.validate(person);
log.info("called validate, result: {}", errors);
return null;
}
}
// UsingValidationApplication.java nothing changed here
package com.example.usingvalidation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MyController {
private final Logger log = LoggerFactory.getLogger(this.getClass());
private final LocalValidatorFactoryBean validatorFactory;
@Autowired
public MyController(LocalValidatorFactoryBean validatorFactory) {
this.validatorFactory = validatorFactory;
}
@GetMapping("/")
public Person getPerson(@RequestBody Person person) {
log.info("calling validate");
validatorFactory.validate(person);
return null;
}
}
If I hit the endpoint to trigger validation, nothing happens. I see the calling validate
log message. But the errors object is empty.
None of the log messages in PersonValidater
are being logged, so clearly no calls are reaching there.
My question is: How do I register my Validator with Spring so that I can use the Validator?
I have gone through the docs multiple times and hundreds of SO questions (like java - Implementing custom validation logic for a spring boot endpoint using a combination of JSR-303 and Spring's Validator - Stack Overflow) but to no avail.
Additional Info
- If there are any JSR-303 annotations like
@NotNull
then the current setup will pick up errors related to the JSR-303 validations. But that is not what I need, I need it to use my custom validator. - I saw other SO questions where
InitBinder
was used in the controller for registering the validator with Spring. But I don't want to do that as I plan to do these custom validations in the service layer.