131

I'm using JPA 2.0/Hibernate validation to validate my models. I now have a situation where the combination of two fields has to be validated:

public class MyModel {
    public Integer getValue1() {
        //...
    }
    public String getValue2() {
        //...
    }
}

The model is invalid if both getValue1() and getValue2() are null and valid otherwise.

How can I perform this kind of validation with JPA 2.0/Hibernate? With a simple @NotNull annotation both getters must be non-null to pass validation.

Pascal Thivent
  • 562,542
  • 136
  • 1,062
  • 1,124
Daniel Rikowski
  • 71,375
  • 57
  • 251
  • 329
  • 2
    possible duplicate of [Cross field validation with Hibernate Validator (JSR 303)](http://stackoverflow.com/questions/1972933/cross-field-validation-with-hibernate-validator-jsr-303) – Steve Chambers Jan 15 '14 at 08:57

5 Answers5

131

For multiple properties validation, you should use class-level constraints. From Bean Validation Sneak Peek part II: custom constraints:

Class-level constraints

Some of you have expressed concerns about the ability to apply a constraint spanning multiple properties, or to express constraint which depend on several properties. The classical example is address validation. Addresses have intricate rules:

  • a street name is somewhat standard and must certainly have a length limit
  • the zip code structure entirely depends on the country
  • the city can often be correlated to a zipcode and some error checking can be done (provided that a validation service is accessible)
  • because of these interdependencies a simple property level constraint does to fit the bill

The solution offered by the Bean Validation specification is two-fold:

  • it offers the ability to force a set of constraints to be applied before an other set of constraints through the use of groups and group sequences. This subject will be covered in the next blog entry
  • it allows to define class level constraints

Class level constraints are regular constraints (annotation / implementation duo) which apply on a class rather than a property. Said differently, class-level constraints receive the object instance (rather than the property value) in isValid.

@AddressAnnotation 
public class Address {
    @NotNull @Max(50) private String street1;
    @Max(50) private String street2;
    @Max(10) @NotNull private String zipCode;
    @Max(20) @NotNull String city;
    @NotNull private Country country;
    
    ...
}

@Constraint(validatedBy = MultiCountryAddressValidator.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface AddressAnnotation {
    String message() default "{error.address}";
    Class<?>[] groups() default { };
    Class<? extends Payload>[] payload() default { };
}

public class MultiCountryAddressValidator implements ConstraintValidator<AddressAnnotation, Address> {
    public void initialize(AddressAnnotation constraintAnnotation) {
    // initialize the zipcode/city/country correlation service
    }

    /**
     * Validate zipcode and city depending on the country
     */
    public boolean isValid(Address object, ConstraintValidatorContext context) {
        if (!(object instanceof Address)) {
            throw new IllegalArgumentException("@AddressAnnotation only applies to Address objects");
        }
        Address address = (Address) object;
        Country country = address.getCountry();
        if (country.getISO2() == "FR") {
            // check address.getZipCode() structure for France (5 numbers)
            // check zipcode and city correlation (calling an external service?)
            return isValid;
        } else if (country.getISO2() == "GR") {
            // check address.getZipCode() structure for Greece
            // no zipcode / city correlation available at the moment
            return isValid;
        }
        // ...
    }
}

The advanced address validation rules have been left out of the address object and implemented by MultiCountryAddressValidator. By accessing the object instance, class level constraints have a lot of flexibility and can validate multiple correlated properties. Note that ordering is left out of the equation here, we will come back to it in the next post.

The expert group has discussed various multiple properties support approaches: we think the class level constraint approach provides both enough simplicity and flexibility compared to other property level approaches involving dependencies. Your feedback is welcome.

t0r0X
  • 4,212
  • 1
  • 38
  • 34
Pascal Thivent
  • 562,542
  • 136
  • 1,062
  • 1,124
  • 17
    The interface ConstraintValidator and the annotation @Constraint have been inverted in the example. And is valid() takes 2 parameters. – Guillaume Husta May 05 '14 at 15:09
  • 2
    `TYPE` and `RUNTIME` should be replaced with `ElementType.TYPE` and `RetentionPolicy.RUNTIME`, respectively. – mark.monteiro Sep 08 '16 at 17:06
  • 3
    @mark.monteiro You can use static imports: `import static java.lang.annotation.ElementType.*;` and `import static java.lang.annotation.RetentionPolicy.*;` – cassiomolin Jun 02 '17 at 09:42
  • 2
    I've rewritten the example to work with Bean Validation. Have a look [here](https://stackoverflow.com/a/44326568/1426227). – cassiomolin Jun 02 '17 at 10:24
  • The instanceof check will fail for null inputs. This is an implicit constraint of `@Address`. I think this is bad style, because users may want to explicitly add this constraint using `@NotNull`. For this reason I think ConstraintValidators should always return true for null inputs. – britter Mar 23 '18 at 12:30
  • 1
    The parameters of annotation are not within specification correct, because there has to be a message, groups, and payload like was mentioned by Cassio under this answer. – Peter S. Apr 24 '18 at 13:12
  • 1
    How the error messages are shown incase of field level errors – Pradeep Sep 17 '19 at 23:19
  • How can we do this via xml configuration? I tried using the element and got this error: ```Caused by: org.xml.sax.SAXParseException; lineNumber: 46; columnNumber: 10; cvc-complex-type.2.4.a: Invalid content was found starting with element 'class'. One of '{"http://jboss.org/xml/ns/javax/validation/mapping":field, "http://jboss.org/xml/ns/javax/validation/mapping":getter, "http://jboss.org/xml/ns/javax/validation/mapping":constructor, "http://jboss.org/xml/ns/javax/validation/mapping":method}' is expected``` – Shubham Kumar Jul 07 '20 at 12:44
  • Since `ConstraintValidator` is generic `instanceof`-check has no sense, method `isValid()` expects an argument of the specified type. – Alexander Ivanchenko Dec 20 '22 at 14:22
53

To work properly with Bean Validation, the example provided in Pascal Thivent's answer could be rewritten as follows:

@ValidAddress
public class Address {

    @NotNull
    @Size(max = 50)
    private String street1;

    @Size(max = 50)
    private String street2;

    @NotNull
    @Size(max = 10)
    private String zipCode;

    @NotNull
    @Size(max = 20)
    private String city;

    @Valid
    @NotNull
    private Country country;

    // Getters and setters
}
public class Country {

    @NotNull
    @Size(min = 2, max = 2)
    private String iso2;

    // Getters and setters
}
@Documented
@Target(TYPE)
@Retention(RUNTIME)
@Constraint(validatedBy = { MultiCountryAddressValidator.class })
public @interface ValidAddress {

    String message() default "{com.example.validation.ValidAddress.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}
public class MultiCountryAddressValidator 
       implements ConstraintValidator<ValidAddress, Address> {

    public void initialize(ValidAddress constraintAnnotation) {

    }

    @Override
    public boolean isValid(Address address, 
                           ConstraintValidatorContext constraintValidatorContext) {

        Country country = address.getCountry();
        if (country == null || country.getIso2() == null || address.getZipCode() == null) {
            return true;
        }

        switch (country.getIso2()) {
            case "FR":
                return // Check if address.getZipCode() is valid for France
            case "GR":
                return // Check if address.getZipCode() is valid for Greece
            default:
                return true;
        }
    }
}
cassiomolin
  • 124,154
  • 35
  • 280
  • 359
  • How to bootstrap or invoke custom validator in a WebSphere restful project for a CDI bean? I have written all but custom constraint is not working or invoked – BalaajiChander Jun 14 '18 at 06:43
  • I am stuck with a similar validation, but my `isoA2Code` is stored in DB `Country` table. Is it a good idea make a DB call from here? Also, I would like to link them after validation because `Address` `belongs_to` a `Country` and I want `address` entry to have `country` table's foreign key. How would I link the country to address? – krozaine Aug 30 '19 at 12:34
  • Note that when you would set a type validation annotation at a wrong object, an exception will be thrown by the Bean Validation framework. For example if you would set the `@ValidAddress` annotation at the Country object, you would get a `No validator could be found for constraint 'com.example.validation.ValidAddress' validating type 'com.example.Country'` exception. – Jacob van Lingen Jul 14 '20 at 10:17
24

You can use @javax.validation.constraints.AssertTrue validations like this:

public class MyModel {
    
    private String value1;
    private String value2;

    @AssertTrue(message = "Values are invalid")
    private boolean isValid() {
        return value1 != null || value2 != null;
    }
}
nyxz
  • 6,918
  • 9
  • 54
  • 67
J.F.
  • 13,927
  • 9
  • 27
  • 65
  • 1
    Thanks for this solution. Initially I wanted to go with `@ScriptAssert`, but some beans have many rules needing validation, so this is much more intuitive: one validation method per rule. – t0r0X Nov 15 '21 at 00:23
  • 2
    Thank you, this is way simpler than writing a custom `ConstraintValidator` and it also needs less code. I think this should be preferred over the accepted solution for this simple use case. – Balu May 29 '23 at 15:16
13

A custom class level validator is the way to go, when you want to stay with the Bean Validation specification, example here.

If you are happy to use a Hibernate Validator feature, you could use @ScriptAssert, which is provided since Validator-4.1.0.Final. Exceprt from its JavaDoc:

Script expressions can be written in any scripting or expression language, for which a JSR 223 ("Scripting for the JavaTM Platform") compatible engine can be found on the classpath.

Example:

@ScriptAssert(lang = "javascript", script = "_this.value1 != null || _this != value2)")
public class MyBean {
  private String value1;
  private String value2;
}
t0r0X
  • 4,212
  • 1
  • 38
  • 34
Hardy
  • 18,659
  • 3
  • 49
  • 65
  • Yes, and Java 6 includes Rhino (JavaScript engine) so you can use JavaScript as the expression language without adding extra dependencies. –  Apr 06 '11 at 07:23
  • 3
    [Here](http://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#section-class-level-constraints) is an example of how to create one such validation with Hibernate Validator 5.1.1.Final – Ivan Hristov Jun 18 '14 at 08:33
0

Programing Language : Java

This is a solution that helped me.

Requirement :

  1. On UI there is a table which contains List of Objects which is having Maping to multiple Tables/Objects with fk relationship.

  2. Now the validation is out of multiple fks there are only 3 columns which can't be duplicated. I mean the combination of 3 can't be duplicated.

Note : As I am working on Custom Framework on Java there is no option to use HashCode or equals. If I will use array index iteration that will increase the time complexity which I don't want.

Solution:

I have prepared a String , which is a custom String that contains ID of FK1#ID of FK2#ID of FK3 Ex: the String will form like -> 1000L#3000L#1300L#

This String, we will add to a set by using add() of set which will return false if duplicate comes up.

Based on this flag we can throw validation message.

This helped me. Some scenario and restriction comes where DS may not help.

Dharman
  • 30,962
  • 25
  • 85
  • 135