Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I validate two or more fields in combination?

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.

like image 275
Daniel Rikowski Avatar asked May 06 '10 14:05

Daniel Rikowski


People also ask

What is a field validator?

Field validation is an automated process of ascertaining that each field contains the correct value before the form is accepted. The concept is straightforward.


2 Answers

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.

like image 127
Pascal Thivent Avatar answered Oct 19 '22 18:10

Pascal Thivent


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;         }     } } 
like image 45
cassiomolin Avatar answered Oct 19 '22 18:10

cassiomolin