Search code examples
javaspringhibernate-validator

How to stop execution of the second validator if first one fails?


I have a Car class and I want it to be validated by two different custom validators in order. I am setting the first validator on top of the class and the other one from validation-constraints-car.xml file.

@Validator1
public class Car {
    private static final long serialVersionUID = 5535968331666441498L;
    
    ...
}
<constraint-mappings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                     xsi:schemaLocation="http://jboss.org/xml/ns/javax/validation/mapping validation-mapping-1.0.xsd"
                     xmlns="http://jboss.org/xml/ns/javax/validation/mapping">
    <bean class="com.galery.Car" ignore-annotations="false">
        <class ignore-annotations="false">
            <constraint annotation="com.galery.validation.specific.Validator2"></constraint>
        </class>
    </bean>

</constraint-mappings>

When the first validator fails, I don't want to execute the second validator. Right now, even if the first one fails it executes the second one and returns the messages for both of the validators. Here is my annotation interfaces and the controller method.

@RequestMapping(value = "....", method = RequestMethod.POST)
public void validateCar(@Valid @RequestBody Car car) {
}
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(
    validatedBy = {Validator1Impl.class}
)
public @interface Validator1{
    String message() default "{validator1.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}
@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(
        validatedBy = {Validator2Impl.class}
)
@Order(value = Ordered.HIGHEST_PRECEDENCE)
public @interface Validator2{
    String message() default "{validator1.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

How can I achieve what I want? Is there any way that I can lookup the message of previous validator in ConstraintValidatorContext?

 @Override
 public boolean isValid(Car value, ConstraintValidatorContext context) {
  ...
}

Solution

  • ============================== right code
    My code use dto "student" to verify.
    student dto

    @Getter
    @Setter
    public class Student implements Serializable{
    
        private static final long serialVersionUID = 1L;
        
        private String name;
        private Integer grade;
    
    }
    

    StudentValidator

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import org.springframework.validation.Errors;
    import org.springframework.validation.ValidationUtils;
    import org.springframework.validation.Validator;
    
    
    @Component
    public class StudentValidator implements Validator {
    
        
        @Override
        public boolean supports(Class<?> clazz) {
            boolean result =  Student.class.equals(clazz);
            return result;
        }
    
        @Override
        public void validate(Object target, Errors errors) {
            Student student = (Student) target;
            ValidationUtils.rejectIfEmpty(errors, "name","name must not be empty");
        }
    }
    

    StudentValidator2. if having error, not to validate.

    @Component
    public class StudentValidator2 implements Validator {
    
        @Autowired
        private MessageSource messageSource;
        
        @Override
        public boolean supports(Class<?> clazz) {
            boolean result =  Student.class.equals(clazz);
            return result;
        }
    
        @Override
        public void validate(Object target, Errors errors) {
            if(errors.hasErrors()){ // if having error, not go ahead
                return ;
            }
            
            Student student = (Student) target;
            if (student.getGrade() <= 0) {
                errors.rejectValue("grade", "grade must be more than 0");
            }
        }
    }
    

    StudentController

    @RestController("/")
    public class StudentController {
    
        @Autowired
        private StudentValidator studentValidator;
    
        @Autowired
        private StudentValidator2 studentValidator2;
    
        @InitBinder(value = "student")
        void initStudentValidator(WebDataBinder binder) {
            binder.addValidators(studentValidator, studentValidator2);
    
        }
    
        @PostMapping("/student")
        public ResponseEntity<Student> saveStudent(@RequestBody @Valid Student student) {
            // Other logic here(Calling the service layer,etc.)
            return new ResponseEntity<>(student, HttpStatus.CREATED);
        }
    }
    

    use postMan to do post to "/student" and no params. Its response as below

    {
        "status": "BAD_REQUEST",
        "error": "Validation failed",
        "count": 1,
        "errors": [
            "name must not be empty"
        ]
    }
    

    ======================================= the below is not right
    I think you should use WebDataBinder and Override validate() method to implement it. Please ref here. The below I give some pseudocode

    1. create a customized WebDataBinder called MyWebDataBinder. In that, override validate() method.
    import org.springframework.stereotype.Component;
    import org.springframework.util.Assert;
    import org.springframework.validation.BindingResult;
    import org.springframework.validation.Validator;
    import org.springframework.web.bind.WebDataBinder;
    
    @Component
    public class MyWebDataBinder extends WebDataBinder {
    
        public MyWebDataBinder(Object target) {
            super(target);
        }
    
        public MyWebDataBinder(Object target, String objectName) {
            super(target, objectName);
        }
    
        @Override
        public void validate() {
            Object target = getTarget();
            Assert.state(target != null, "No target to validate");
            BindingResult bindingResult = getBindingResult();
            // Call each validator with the same binding result
            for (Validator validator : getValidators()) {
                validator.validate(target, bindingResult);
                if(bindingResult.hasErrors()) // jump out when if having error 
                    break;
            }
        }
    }
    
    1. in your controller, inject MyWebDataBinder and Validator1 & Validator2
    
    @Controller
    public class CustomerController {
    
       @Autowired
       Validator1 validator1
    
       @Autowired
       Validator2 validator2
    
        @InitBinder(value = "car")
        void initCarValidator(@Qualifier("myWebDataBinder")WebDataBinder binder) {
            binder.addValidators(validator1);
            binder.addValidators(validator2);
        }
    
        @RequestMapping(value = "....", method = RequestMethod.POST)
         public void validateCar(@Valid @RequestBody Car car) {
           // .... your code
        }