springspring-mvcspring-validatorspring-validation

Spring custom validator with dependencies on other fields


We are using spring custom validator for our request object used in our controller endpoint. We implemented it the same way as how its done in the link below:

https://www.baeldung.com/spring-mvc-custom-validator

The problem we are facing is, it can't work if the particular field has dependencies on other input fields as well. For example, we have the code below as the request object for our controller endpoint:

public class FundTransferRequest {

     private String accountTo;
     private String accountFrom;
     private String amount;
     
     @CustomValidator
     private String reason;

     private Metadata metadata;

}

public class Metadata {
   private String channel; //e.g. mobile, web, etc.
}

Basically @CustomValidator is our custom validator class and the logic we want is, if the supplied channel from Metadata is "WEB". The field "reason" of the request won't be required. Else, it will be required.

Is there a way to do this? I've done additional research and can't see any that handles this type of scenario.


Solution

  • Obviously if you need access to multiple fields in your custom validator, you have to use a class-level annotation.

    The same very article you mentioned has an example of that: https://www.baeldung.com/spring-mvc-custom-validator#custom-class-level-validation

    In your case it might look something like this:

    @Constraint(validatedBy = CustomValidator.class)
    @Target({ ElementType.TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface CustomValidation {
    
        String message() default "Reason required";
        String checkedField() default "metadata.channel";
        String checkedValue() default "WEB";
        String requiredField() default "reason";
    
        Class<?>[] groups() default {};
        Class<? extends Payload>[] payload() default {};
    }
    
    package com.example.demo;
    
    import org.springframework.beans.BeanWrapperImpl;
    import org.springframework.stereotype.Component;
    
    import javax.validation.ConstraintValidator;
    import javax.validation.ConstraintValidatorContext;
    
    /*
    If the supplied channel from Metadata is "WEB". The field "reason" of the request won't be required.
    Else, it will be required.
     */
    @Component
    public class CustomValidator implements ConstraintValidator<CustomValidation, Object> {
        private String checkedField;
        private String checkedValue;
        private String requiredField;
    
        @Override
        public void initialize(CustomValidation constraintAnnotation) {
            this.checkedField = constraintAnnotation.checkedField();
            this.checkedValue = constraintAnnotation.checkedValue();
            this.requiredField = constraintAnnotation.requiredField();
        }
    
        @Override
        public boolean isValid(Object value, ConstraintValidatorContext context) {
            Object checkedFieldValue = new BeanWrapperImpl(value)
                    .getPropertyValue(checkedField);
            Object requiredFieldValue = new BeanWrapperImpl(value)
                    .getPropertyValue(requiredField);
    
            return checkedFieldValue != null && checkedFieldValue.equals(checkedValue) || requiredFieldValue != null;
        }
    }
    
    

    And the usage will be:

    @CustomValidation
    public class FundTransferRequest {
    ...
    

    or with parameters specified:

    @CustomValidation(checkedField = "metadata.channel", 
            checkedValue = "WEB", 
            requiredField = "reason", 
            message = "Reason required")
    public class FundTransferRequest {
    ...