Dogukan Evcil
Dogukan Evcil

Reputation: 373

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

Upvotes: 2

Views: 1496

Answers (1)

jacky-neo
jacky-neo

Reputation: 853

============================== 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
    }

Upvotes: 2

Related Questions