128

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.

Vladimir Vagaytsev
  • 2,871
  • 9
  • 33
  • 36
Eric
  • 1,383
  • 2
  • 9
  • 5

6 Answers6

183

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

lucapette
  • 20,564
  • 6
  • 65
  • 59
Audrius Meškauskas
  • 20,936
  • 12
  • 75
  • 93
  • 1
    I used your method and it works, but I can't figure out how to get the message. Would you happen to know? – anaBad Jul 28 '16 at 09:03
  • 15
    This was by far the most efficient of options. Thanks! @anaBad: The AssertTrue annotation can take a custom message, just as other constraint annotations. – ernest_k Aug 01 '16 at 07:11
  • @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: `
    ` But it throws an error.
    – anaBad Aug 01 '16 at 09:39
  • @ErnestKiwele Never mind I figured it out, I made a boolean attribute that is set when setReference() is called. – anaBad Aug 01 '16 at 11:34
  • I downvoted your post accidentally ;) Can you edit it, so I could upvote it? – Krzysztof Atłasik May 29 '17 at 13:21
  • 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 property – naXa stands with Ukraine Jun 09 '17 at 16:22
  • Do not name any other method so that Spring would confuse it for the setter or getter of the same property for your bean – Audrius Meškauskas Jun 09 '17 at 17:17
  • 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 – AbstractVoid Oct 21 '17 at 11:42
  • 6
    i had to make the method public – tibi Mar 07 '18 at 12:59
  • it work, but how can we get set spring Errors rejectValue stuff? – min Feb 23 '19 at 08:42
  • What is the assert true annotation? – Dragas Jun 17 '20 at 10:21
  • 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. – Stefan Haberl Sep 25 '20 at 07:09
  • One may also use `@AssertFalse` and "simplify" the boolean expression to `return someField == something && otherField == null;` – Julien Nov 24 '21 at 11:29
129

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)

Slava Semushin
  • 14,904
  • 7
  • 53
  • 69
  • 2
    @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. – Slava Semushin Jun 08 '17 at 15:10
  • 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. – Ben Jun 08 '17 at 15:21
  • 1
    Yes, that what I mean when I said "in this case validator become less generic". – Slava Semushin Jun 08 '17 at 22:04
  • I want to use this trick for protoBuffer classes. this is very helpful (: – Saeed May 21 '20 at 07:29
  • Nice solution. Very helpful to build custom annotation! – Vishwa Jul 03 '20 at 13:31
  • 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! – Stefan Haberl Sep 25 '20 at 07:11
  • @StefanHaberl suggest it to the jakarta team, since this is managed by them now – xenoterracide Dec 22 '20 at 21:10
  • @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. – xenoterracide Dec 22 '20 at 21:11
28

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.

user11153
  • 8,536
  • 5
  • 47
  • 50
  • 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... – Jules May 12 '17 at 23:34
  • 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? : https://stackoverflow.com/questions/44520306/hibernate-validator-group-sequence-provider-getdefaultsequenceprovider-gets-nul – user238607 Jul 17 '17 at 19:25
  • 3
    @user238607 getValidationGroups(MyCustomForm myCustomForm) will call many time per bean instance and it some time pass null. You just do ignore if it pass null. – pramoth Mar 28 '18 at 07:21
  • 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 this – Ekansh Rastogi Feb 10 '22 at 11:54
12

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}
jokarl
  • 1,913
  • 2
  • 24
  • 52
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.

Michael Wyraz
  • 3,638
  • 1
  • 27
  • 25
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";
        }
    }
}
Ibrahim AlTamimi
  • 624
  • 5
  • 15