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.
CodePudding user response:
You should annotate your controller with the @Validated
annotation. Don't forget to also add the spring-boot-starter-validation
to your pom.
CodePudding user response:
The primary reason this isn't working for you is that you haven't registered your validator with the DataBinder.
Make a couple of changes to your controller. Instead of auto wiring the LocalValidatorFactoryBean
, auto wire your validator(s) into the controller and register them with the DataBinder.
@Autowired
private PersonValidator personValidator;
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.addValidators(personValidator);
}
Your controller method will be simpler too as you no longer need to explicitly call the ValidatorFactory, Spring will invoke the validators automatically when you add the @Valid annotation to your method parameter. Add the BindingResult
parameter to the method and all errors that come from the validators will be present in the BindingResult errors, this includes errors caused by the javax validations like, @Min, @Max, @NotNull, etc.
@GetMapping("/")
public Person getPerson(@RequestBody @Valid Person person, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
log.info(bindingResult.getAllErrors());
}
return null;
}
As you want to do this in the service layer, you're forced to write your own logic to handle this. Spring doesn't do any magic as far as calling the custom validations. This is intentional, the ingress into your application is through the controller, this is the one place where you have limited control over the data being input so if you want to validate, it should be handled here. Every mutation of the Person object downstream of the controller you have total control over. If you feel that you absolutely must validate in the service layer, then you're going to be writing this yourself and frankly, I wouldn't use an implementation of Spring's Validator for this. If you're deadset on doing this in the service layer, here's a way to pull it off.
Create an annotation to apply to your Person
class
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
@Documented
@Constraint(validatedBy = PersonValidator.class)
@Target({TYPE})
@Retention(RUNTIME)
public @interface ValidPerson {
String message() default "This isn't correct";
Class[] groups() default {};
Class[] payload() default {};
}
Add the above annotation to your Person
class.
@ValidPerson
public class Person {
Modify your PersonValidator to be a ConstraintValidator
, I've thrown together an implementation that validates two of the fields on Person.
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.springframework.util.ObjectUtils;
public class PersonValidator implements ConstraintValidator<ValidPerson, Person> {
@Override
public void initialize(ValidPerson constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}
@Override
public boolean isValid(Person value, ConstraintValidatorContext context) {
boolean isErrored = false;
if (ObjectUtils.isEmpty(value.getLastName())) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("lastName can't be empty").addConstraintViolation();
isErrored = true;
}
if (value.getAge() < 0) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("You're not old enough to be alive").addConstraintViolation();
isErrored = true;
}
return !isErrored;
}
}
In your service class, inject a Validator and call it in your method, this will invoke the ConstraintValidator
that you have defined and added to your Person
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class PersonService {
@Autowired
private Validator validator;
public Person updatePerson(Person person) {
Set<ConstraintViolation<Person>> validate = validator.validate(person);
return person;
}
}
You can do some fancy things with AOP to do this automatically the way that Spring does on the Controller side of things, but I'll leave that for you to discover.