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

6

138

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.

Bedew answered 14/2, 2012 at 21:30 Comment(0)
I
130

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: Cross field validation with Hibernate Validator (JSR 303)

Ist answered 15/2, 2012 at 4:14 Comment(8)
@Benedictus This example will only work with strings but you can modify it to work with any objects. There are 2 ways: 1) parametrize validator with class that you want to validate (instead of Object). In this case, you even don't need to use reflection for getting the values but in this case validator become less generic 2) use BeanWrapperImp from Spring Framework (or other libraries) and its getPropertyValue() method. In this case you will be able to get a value as Object and cast to any type that you need.Ist
Yeah, but you can't have Object as annotation parameter, So you'll need a bunch of different annotations for each type you want to validate.Seller
Yes, that what I mean when I said "in this case validator become less generic".Ist
I want to use this trick for protoBuffer classes. this is very helpful (:Sams
Nice solution. Very helpful to build custom annotation!Copolymerize
Unfortunately this looks like the only valid solution. So much boilerplate code for such a simple requirement! I'm sure, the JSR 303 team could do better!Thant
@StefanHaberl suggest it to the jakarta team, since this is managed by them nowCough
@SlavaSemushin is there a way to get the field inside the constraint validator if you put the decorator on the field? I don't immediately see a fieldname, or the like available.Cough
B
194

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'.

Boles answered 10/6, 2016 at 12:1 Comment(15)
I used your method and it works, but I can't figure out how to get the message. Would you happen to know?Zebada
This was by far the most efficient of options. Thanks! @anaBad: The AssertTrue annotation can take a custom message, just as other constraint annotations.Latishalatitude
@ErnestKiwele Thanks for answering, but my problem isn't with setting the message but getting it in my jsp. I have the following function the model: @AssertTrue(message="La reference doit etre un URL") public boolean isReferenceOk() { return origine!=Origine.Evolution||reference.contains("http://jira.bcaexpertise.org"); } And this in my jsp: <th><form:label path="reference"><s:message code="reference"/></form:label></th><td><form:input path="reference" cssErrorClass="errorField"/><br/><form:errors path="isReferenceOk" cssClass="error"/></td> But it throws an error.Zebada
@ErnestKiwele Never mind I figured it out, I made a boolean attribute that is set when setReference() is called.Zebada
I downvoted your post accidentally ;) Can you edit it, so I could upvote it?Agulhas
I get this exception "Bean property X.isY is not readable or has an invalid getter method: Does the return type of the getter match the parameter type of the setter?" when I add such validation to a nested propertyMonroy
Do not name any other method so that Spring would confuse it for the setter or getter of the same property for your beanFutrell
This helped me, but in order to output this in the form I had to change this method from private to public and then in the view I had to refer to this method without 'is' part. So I had method like "public boolean isSomethingValid()" and in the form I was outputting this like <span class="error" th:if="${#fields.hasErrors('someForm.somethingValid')}" th:errors="*{someForm.somethingValid}>Fermin
i had to make the method publicErechtheus
it work, but how can we get set spring Errors rejectValue stuff?Australopithecus
What is the assert true annotation?Guimpe
Easy workaround (another one required for Hibernate Validator :X), but it rather builds around the framework: I'd rather expect to have a constraint violation on the single invalid properties, not on a public - nonesense - ok Property.Thant
One may also use @AssertFalse and "simplify" the boolean expression to return someField == something && otherField == null;Molybdate
I am facing a similar issue where value of one field( a = 'emp1' ) changes the not null constraint for other fields(email, phno, address). How do i set the message specifying which field is violating the not null(message = phno/address/email "cannot be null when a =emp1 ") constraint when a is emp1?Scaleboard
1. use boolean (if you use Boolean, it won't work) 2. if you pass message, the key used will be function name after isEpiscopalian
I
130

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: Cross field validation with Hibernate Validator (JSR 303)

Ist answered 15/2, 2012 at 4:14 Comment(8)
@Benedictus This example will only work with strings but you can modify it to work with any objects. There are 2 ways: 1) parametrize validator with class that you want to validate (instead of Object). In this case, you even don't need to use reflection for getting the values but in this case validator become less generic 2) use BeanWrapperImp from Spring Framework (or other libraries) and its getPropertyValue() method. In this case you will be able to get a value as Object and cast to any type that you need.Ist
Yeah, but you can't have Object as annotation parameter, So you'll need a bunch of different annotations for each type you want to validate.Seller
Yes, that what I mean when I said "in this case validator become less generic".Ist
I want to use this trick for protoBuffer classes. this is very helpful (:Sams
Nice solution. Very helpful to build custom annotation!Copolymerize
Unfortunately this looks like the only valid solution. So much boilerplate code for such a simple requirement! I'm sure, the JSR 303 team could do better!Thant
@StefanHaberl suggest it to the jakarta team, since this is managed by them nowCough
@SlavaSemushin is there a way to get the field inside the constraint validator if you put the decorator on the field? I don't immediately see a fieldname, or the like available.Cough
S
29

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.

Skewness answered 25/1, 2017 at 10:39 Comment(4)
Interesting way of doing it. The answer could do with more explanation of how it works, though, because I had to read it twice before I saw what was going on...Chaechaeronea
Hi, I implemented your solution but facing an issue. No object is being passed to the getValidationGroups(MyCustomForm myCustomForm) method. Could you possibly help here? : #44520806Electrokinetic
@Electrokinetic getValidationGroups(MyCustomForm myCustomForm) will call many time per bean instance and it some time pass null. You just do ignore if it pass null.Sporogonium
this seems a way better solution to me, however, the answer has very few details how this works, maybe a sample validator could have helped and saved a lot of time reading about thisErector
P
13

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}
Polemoniaceous answered 22/5, 2018 at 10:10 Comment(0)
J
3

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.

Jaqitsch answered 25/11, 2015 at 16:8 Comment(0)
R
0

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";
        }
    }
}
Ravel answered 23/9, 2020 at 13:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.