JSR 303 Validation, If one field equals "something", then these other fields should not be null

JavaValidationBean Validation

Java Problem Overview


I'm looking to do a little custom validation with JSR-303 javax.validation.

I have a field. And If a certain value is entered into this field I want to require that a few other fields are not null.

I'm trying to figure this out. Not sure exactly what I would call this to help find an explanation.

Any help would be appreciated. I am pretty new to this.

At the moment I'm thinking a Custom Constraint. But I'm not sure how to test the value of the dependent field from within the annotation. Basically I'm not sure how to access the panel object from the annotation.

public class StatusValidator implements ConstraintValidator<NotNull, String> {

    @Override
	public void initialize(NotNull constraintAnnotation) {}

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
	    if ("Canceled".equals(panel.status.getValue())) {
            if (value != null) {
                return true;
            }
        } else {
    		return false;
        }
    }
}

It's the panel.status.getValue(); giving me trouble.. not sure how to accomplish this.

Java Solutions


Solution 1 - Java

Define method that must validate to true and put the @AssertTrue annotation on the top of it:

  @AssertTrue
  private boolean isOk() {
    return someField != something || otherField != null;
  }

The method must start with 'is'.

Solution 2 - Java

In this case I suggest to write a custom validator, which will validate at class level (to allow us get access to object's fields) that one field is required only if another field has particular value. Note that you should write generic validator which gets 2 field names and work with only these 2 fields. To require more than one field you should add this validator for each field.

Use the following code as an idea (I've not test it).

  • Validator interface

    /**
     * Validates that field {@code dependFieldName} is not null if
     * field {@code fieldName} has value {@code fieldValue}.
     **/
    @Target({TYPE, ANNOTATION_TYPE})
    @Retention(RUNTIME)
    @Repeatable(NotNullIfAnotherFieldHasValue.List.class) // only with hibernate-validator >= 6.x
    @Constraint(validatedBy = NotNullIfAnotherFieldHasValueValidator.class)
    @Documented
    public @interface NotNullIfAnotherFieldHasValue {
    	
        String fieldName();
        String fieldValue();
        String dependFieldName();
        
        String message() default "{NotNullIfAnotherFieldHasValue.message}";
        Class<?>[] groups() default {};
        Class<? extends Payload>[] payload() default {};
        
        @Target({TYPE, ANNOTATION_TYPE})
        @Retention(RUNTIME)
        @Documented
        @interface List {
        	NotNullIfAnotherFieldHasValue[] value();
        }
        
    }
    
  • Validator implementation

    /**
     * Implementation of {@link NotNullIfAnotherFieldHasValue} validator.
     **/
    public class NotNullIfAnotherFieldHasValueValidator
    	implements ConstraintValidator<NotNullIfAnotherFieldHasValue, Object> {
    	
    	private String fieldName;
    	private String expectedFieldValue;
    	private String dependFieldName;
    	
    	@Override
    	public void initialize(NotNullIfAnotherFieldHasValue annotation) {
    		fieldName          = annotation.fieldName();
    		expectedFieldValue = annotation.fieldValue();
    		dependFieldName    = annotation.dependFieldName();
    	}
    	
    	@Override
    	public boolean isValid(Object value, ConstraintValidatorContext ctx) {
    		
    		if (value == null) {
    			return true;
    		}
    		
    		try {
    			String fieldValue       = BeanUtils.getProperty(value, fieldName);
    			String dependFieldValue = BeanUtils.getProperty(value, dependFieldName);
    			
    			if (expectedFieldValue.equals(fieldValue) && dependFieldValue == null) {
    				ctx.disableDefaultConstraintViolation();
    				ctx.buildConstraintViolationWithTemplate(ctx.getDefaultConstraintMessageTemplate())
    					.addNode(dependFieldName)
    					.addConstraintViolation();
    					return false;
    			}
    		
    		} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException ex) {
    			throw new RuntimeException(ex);
    		}
    		
    		return true;
    	}
    	
    }
    
  • Validator usage example (hibernate-validator >= 6 with Java 8+)

    @NotNullIfAnotherFieldHasValue(
        fieldName = "status",
        fieldValue = "Canceled",
        dependFieldName = "fieldOne")
    @NotNullIfAnotherFieldHasValue(
        fieldName = "status",
        fieldValue = "Canceled",
        dependFieldName = "fieldTwo")
    public class SampleBean {
        private String status;
        private String fieldOne;
        private String fieldTwo;
        
        // getters and setters omitted
    }
    
  • Validator usage example (hibernate-validator < 6; the old example)

    @NotNullIfAnotherFieldHasValue.List({
        @NotNullIfAnotherFieldHasValue(
            fieldName = "status",
            fieldValue = "Canceled",
            dependFieldName = "fieldOne"),
        @NotNullIfAnotherFieldHasValue(
            fieldName = "status",
            fieldValue = "Canceled",
            dependFieldName = "fieldTwo")
    })
    public class SampleBean {
        private String status;
        private String fieldOne;
        private String fieldTwo;
        
        // getters and setters omitted
    }
    

Note that validator implementation uses BeanUtils class from commons-beanutils library but you could also use BeanWrapperImpl from Spring Framework.

See also this great answer: https://stackoverflow.com/questions/1972933/cross-field-validation-with-hibernate-validator-jsr-303

Solution 3 - Java

You should make use of custom DefaultGroupSequenceProvider<T>:

ConditionalValidation.java

// Marker interface
public interface ConditionalValidation {}

MyCustomFormSequenceProvider.java

public class MyCustomFormSequenceProvider
    implements DefaultGroupSequenceProvider<MyCustomForm> {

    @Override
    public List<Class<?>> getValidationGroups(MyCustomForm myCustomForm) {

        List<Class<?>> sequence = new ArrayList<>();

        // Apply all validation rules from ConditionalValidation group
        // only if someField has given value
        if ("some value".equals(myCustomForm.getSomeField())) {
            sequence.add(ConditionalValidation.class);
        }

        // Apply all validation rules from default group
        sequence.add(MyCustomForm.class);

        return sequence;
    }
}

MyCustomForm.java

@GroupSequenceProvider(MyCustomFormSequenceProvider.class)
public class MyCustomForm {

    private String someField;

    @NotEmpty(groups = ConditionalValidation.class)
    private String fieldTwo;

    @NotEmpty(groups = ConditionalValidation.class)
    private String fieldThree;

    @NotEmpty
    private String fieldAlwaysValidated;


    // getters, setters omitted
}

See also related question on this topic.

Solution 4 - Java

Here's my take on it, tried to keep it as simple as possible.

The interface:

@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = OneOfValidator.class)
@Documented
public @interface OneOf {

	String message() default "{one.of.message}";
    
    Class<?>[] groups() default {};
    
    Class<? extends Payload>[] payload() default {};
    
	String[] value();
}

Validation implementation:

public class OneOfValidator implements ConstraintValidator<OneOf, Object> {

	private String[] fields;

	@Override
	public void initialize(OneOf annotation) {
		this.fields = annotation.value();
	}

	@Override
	public boolean isValid(Object value, ConstraintValidatorContext context) {
	
		BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(value);
	
		int matches = countNumberOfMatches(wrapper);
	
		if (matches > 1) {
			setValidationErrorMessage(context, "one.of.too.many.matches.message");
			return false;
		} else if (matches == 0) {
			setValidationErrorMessage(context, "one.of.no.matches.message");
			return false;
		}
	
		return true;
	}

	private int countNumberOfMatches(BeanWrapper wrapper) {
		int matches = 0;
		for (String field : fields) {
			Object value = wrapper.getPropertyValue(field);
			boolean isPresent = detectOptionalValue(value);

			if (value != null && isPresent) {
				matches++;
			}
		}
		return matches;
	}

	private boolean detectOptionalValue(Object value) {
		if (value instanceof Optional) {
			return ((Optional) value).isPresent();
		}
		return true;
	}

	private void setValidationErrorMessage(ConstraintValidatorContext context, String template) {
		context.disableDefaultConstraintViolation();
		context
			.buildConstraintViolationWithTemplate("{" + template + "}")
			.addConstraintViolation();
	}

}

Usage:

@OneOf({"stateType", "modeType"})
public class OneOfValidatorTestClass {

	private StateType stateType;

	private ModeType modeType;

}

Messages:

one.of.too.many.matches.message=Only one of the following fields can be specified: {value}
one.of.no.matches.message=Exactly one of the following fields must be specified: {value}

Solution 5 - Java

A different approach would be to create a (protected) getter that returns an object containing all dependent fields. Example:

public class MyBean {
  protected String status;
  protected String name;

  @StatusAndSomethingValidator
  protected StatusAndSomething getStatusAndName() {
    return new StatusAndSomething(status,name);
  }
}

StatusAndSomethingValidator can now access StatusAndSomething.status and StatusAndSomething.something and make a dependent check.

Solution 6 - Java

Sample below:

package io.quee.sample.javax;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import javax.validation.ConstraintViolation;
import javax.validation.Valid;
import javax.validation.Validator;
import javax.validation.constraints.Pattern;
import java.util.Set;

/**
 * Created By [**Ibrahim Al-Tamimi **](https://www.linkedin.com/in/iloom/)
 * Created At **Wednesday **23**, September 2020**
 */
@SpringBootApplication
public class SampleJavaXValidation implements CommandLineRunner {
    private final Validator validator;

    public SampleJavaXValidation(Validator validator) {
        this.validator = validator;
    }

    public static void main(String[] args) {
        SpringApplication.run(SampleJavaXValidation.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        Set<ConstraintViolation<SampleDataCls>> validate = validator.validate(new SampleDataCls(SampleTypes.TYPE_A, null, null));
        System.out.println(validate);
    }

    public enum SampleTypes {
        TYPE_A,
        TYPE_B;
    }

    @Valid
    public static class SampleDataCls {
        private final SampleTypes type;
        private final String valueA;
        private final String valueB;

        public SampleDataCls(SampleTypes type, String valueA, String valueB) {
            this.type = type;
            this.valueA = valueA;
            this.valueB = valueB;
        }

        public SampleTypes getType() {
            return type;
        }

        public String getValueA() {
            return valueA;
        }

        public String getValueB() {
            return valueB;
        }

        @Pattern(regexp = "TRUE")
        public String getConditionalValueA() {
            if (type.equals(SampleTypes.TYPE_A)) {
                return valueA != null ? "TRUE" : "";
            }
            return "TRUE";
        }

        @Pattern(regexp = "TRUE")
        public String getConditionalValueB() {
            if (type.equals(SampleTypes.TYPE_B)) {
                return valueB != null ? "TRUE" : "";
            }
            return "TRUE";
        }
    }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionEricView Question on Stackoverflow
Solution 1 - JavaAudrius MeškauskasView Answer on Stackoverflow
Solution 2 - JavaSlava SemushinView Answer on Stackoverflow
Solution 3 - Javauser11153View Answer on Stackoverflow
Solution 4 - JavajokarlView Answer on Stackoverflow
Solution 5 - JavaMichael WyrazView Answer on Stackoverflow
Solution 6 - JavaIbrahim AlTamimiView Answer on Stackoverflow