261

Is there an implementation of (or third-party implementation for) cross field validation in Hibernate Validator 4.x? If not, what is the cleanest way to implement a cross field validator?

As an example, how can you use the API to validate two bean properties are equal (such as validating a password field matches the password verify field).

In annotations, I'd expect something like:

public class MyBean {
  @Size(min=6, max=50)
  private String pass;

  @Equals(property="pass")
  private String passVerify;
}
messivanio
  • 2,263
  • 18
  • 24
Bradley Dwyer
  • 8,102
  • 5
  • 32
  • 28
  • 2
    See http://stackoverflow.com/questions/2781771/how-can-i-validate-two-or-more-fields-in-combination for a type-safe and reflection API-free (imo more elegant) solution on the class level. – Kalle Richter Aug 26 '15 at 13:06

15 Answers15

311

Each field constraint should be handled by a distinct validator annotation, or in other words it's not suggested practice to have one field's validation annotation checking against other fields; cross-field validation should be done at the class level. Additionally, the JSR-303 Section 2.2 preferred way to express multiple validations of the same type is via a list of annotations. This allows the error message to be specified per match.

For example, validating a common form:

@FieldMatch.List({
        @FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match"),
        @FieldMatch(first = "email", second = "confirmEmail", message = "The email fields must match")
})
public class UserRegistrationForm  {
    @NotNull
    @Size(min=8, max=25)
    private String password;

    @NotNull
    @Size(min=8, max=25)
    private String confirmPassword;

    @NotNull
    @Email
    private String email;

    @NotNull
    @Email
    private String confirmEmail;
}

The Annotation:

package constraints;

import constraints.impl.FieldMatchValidator;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Target;

/**
 * Validation annotation to validate that 2 fields have the same value.
 * An array of fields and their matching confirmation fields can be supplied.
 *
 * Example, compare 1 pair of fields:
 * @FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match")
 * 
 * Example, compare more than 1 pair of fields:
 * @FieldMatch.List({
 *   @FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match"),
 *   @FieldMatch(first = "email", second = "confirmEmail", message = "The email fields must match")})
 */
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = FieldMatchValidator.class)
@Documented
public @interface FieldMatch
{
    String message() default "{constraints.fieldmatch}";

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

    Class<? extends Payload>[] payload() default {};

    /**
     * @return The first field
     */
    String first();

    /**
     * @return The second field
     */
    String second();

    /**
     * Defines several <code>@FieldMatch</code> annotations on the same element
     *
     * @see FieldMatch
     */
    @Target({TYPE, ANNOTATION_TYPE})
    @Retention(RUNTIME)
    @Documented
            @interface List
    {
        FieldMatch[] value();
    }
}

The Validator:

package constraints.impl;

import constraints.FieldMatch;
import org.apache.commons.beanutils.BeanUtils;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object>
{
    private String firstFieldName;
    private String secondFieldName;

    @Override
    public void initialize(final FieldMatch constraintAnnotation)
    {
        firstFieldName = constraintAnnotation.first();
        secondFieldName = constraintAnnotation.second();
    }

    @Override
    public boolean isValid(final Object value, final ConstraintValidatorContext context)
    {
        try
        {
            final Object firstObj = BeanUtils.getProperty(value, firstFieldName);
            final Object secondObj = BeanUtils.getProperty(value, secondFieldName);

            return firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
        }
        catch (final Exception ignore)
        {
            // ignore
        }
        return true;
    }
}
Patrick
  • 2,672
  • 3
  • 31
  • 47
Nicko
  • 3,293
  • 1
  • 15
  • 7
  • 1
    Nice implementation with no external dependencies. – AndyT May 26 '10 at 20:37
  • 10
    @AndyT: There is an external dependency on Apache Commons BeanUtils. – GaryF Aug 11 '10 at 08:13
  • Clean implementation, but probably for most purposes @ScriptAssert is much more convenient. It's nice to to be able to define arbitrary constraints without having to create corresponding annotations and validators each time. –  Apr 06 '11 at 07:32
  • 7
    @ScriptAssert doesn't let you build a validation message with a customized path. `context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()).addNode(secondFieldName).addConstraintViolation().disableDefaultConstraintViolation();` Gives the possibility of highlighting the right field (if only JSF would support it). – Peter Davis Apr 13 '11 at 15:25
  • 8
    i used above sample but it doesn't display error message, what is the binding should be in the jsp ? i have binding for password and confirm only, is there anything else needed ? – Mahmoud Saleh Sep 12 '11 at 09:30
  • 7
    `BeanUtils.getProperty` returns a string. The example probably meant to use `PropertyUtils.getProperty` which returns an object. – SingleShot Dec 13 '11 at 19:25
  • 1
    May I use this code in commercial work? Who would I attribute to? – Wes Dec 05 '12 at 20:40
  • 1
    I loved this solution. The only drawback is when I check the errors inside my controller. When I get the result.getFieldErrors(), the ones raised by the annotation are not there. Is there an easy way to make it a FieldError? – Paulo Pedroso May 02 '13 at 13:04
  • nice approach, but it doesn't define the code properly. The answer below by bradhouse demonstrates how to add the error code (the magic is in the if (!matches) block). – Cyril Deba Jul 28 '13 at 16:15
  • 2
    Nice answer, but I've completed it with the answer to this question: http://stackoverflow.com/questions/11890334/cross-field-validation-with-hibernatevalidator-works-fine-but-displays-no-error – maxivis Sep 24 '13 at 00:11
  • Thank you very much for this, it was very useful for my project – TheYann Mar 23 '14 at 10:11
  • 2
    It is possible to use BeanWrapperImpl from Spring instead of BeanUtils from Apache. – pasemes Aug 07 '14 at 13:30
  • This should probably work for a customized path: constraintValidatorContext.disableDefaultConstraintViolation(); constraintValidatorContext .buildConstraintViolationWithTemplate(constraintValidatorContext.getDefaultConstraintMessageTemplate()) .addPropertyNode(secondFieldName) .addConstraintViolation(); – Krishna Sep 18 '14 at 22:04
  • last version of BeanUtils have getProperty method which returns Object – gstackoverflow Apr 19 '15 at 20:15
  • Is there any way to use spring beans in validator? http://stackoverflow.com/questions/40584985/validation-is-in-dictionary – dmitryvim Nov 14 '16 at 09:05
  • You can use this method if you don't want to use external apache BeanUtils library: private Object getValue(Object object, String fieldName) { try { Field field = object.getClass().getDeclaredField(fieldName); field.setAccessible(true); Object value = field.get(object); return value; } catch (Exception e) { e.printStackTrace(); return null; } } – Mehdi Nov 16 '17 at 12:02
  • For someone get here by search, you may use this as dependency directly: `org.apache.struts:struts2-bean-validation-plugin` – Sutra Mar 10 '19 at 15:05
  • For me make StringIndexOutOfBoundsException and satisfied this https://stackoverflow.com/questions/11890334/cross-field-validation-with-hibernatevalidator-displays-no-error-messages – user3480687 Dec 08 '20 at 08:58
192

I suggest you another possible solution. Perhaps less elegant, but easier!

public class MyBean {
  @Size(min=6, max=50)
  private String pass;

  private String passVerify;

  @NotNull
  private LocalDate passExpiry;
  @NotNull
  private LocalDate dateOfJoining;

  @AssertTrue(message = "Fields `pass` and `passVerify` should be equal")
  // Any method name is ok als long it begins with `is`
  private boolean isValidPass() {
    //return pass == null && passVerify == null || pass.equals(passVerify);
    // Since Java 7:
    return Objects.equals(pass, passVerify);
  }

  @AssertTrue(message = "Field `passExpiry` should be later than `dateOfJoining`")
  // Other rules can also be validated in other methods
  private boolean isPassExpiryAfterDateOfJoining() {
    return dateOfJoining.isBefore(passExpiry);
  }
}

The isValid() and isPassExpiryAfterDateOfJoining() methods are invoked automatically by the validator. The property paths reported in the ConstraintViolations will be extracted from the method names: valid and passExpiryAfterDateOfJoining.

t0r0X
  • 4,212
  • 1
  • 38
  • 34
Alberthoven
  • 2,798
  • 1
  • 24
  • 29
  • Interesting. It certainly gets around the complexity of associating field level errors correctly when you're using a class level validator. My only concern would be around i18n. For a given 'type' of validation failure I have generic messages for those types. If you only had one @AssertTrue in your app this would be fine, but otherwise I'm not sure it would work (in a generic sense with i18n). – Bradley Dwyer Jan 20 '10 at 20:59
  • Can't you put a message code into the message attribute, so it goes through the normal validator MessageInterpolator? – GaryF Mar 25 '10 at 15:45
  • 16
    I think this is a mixing of concerns again. The whole point of Bean Validation is to externalize the validation into ConstraintValidators. In this case you have part of the validation logic in the bean itself and part in the Validator framework. The way to go is a class level constraint. Hibernate Validator also offers now a @ScriptAssert which makes the implementation of bean internal dependencies easier. – Hardy May 18 '10 at 07:54
  • 1
    match() is not longer valid naming convention for spring annotations. Please use java bean naming conventions like isValid() – Ganesh Krishnan Nov 06 '13 at 03:56
  • One - drawback - This is not declarative - more programatioc. I can't externalize this .. say to a jsonSchema with validation – bhantol Aug 06 '14 at 19:25
  • From a more theoretical point of view I think Hardy is right (so +1). I actually use this method all the time. From a pragmatic view this is so much simpler than creating a class-level constraint for each specific use case (so +1 to Alberthoven as well). – Henno Vermeulen Jan 13 '15 at 15:02
  • I haven't tested it, but at least with Hibernate Validator, I18N can be done by using a key of ValidationMessage resource bundle inside braces. – Henno Vermeulen Jan 13 '15 at 15:09
  • 10
    My opinion thus far is that the **Bean Validation** JSR is a mixing of concerns. – Dmitry Minkovsky Jul 20 '15 at 21:09
  • 3
    @GaneshKrishnan What if we want to have several such `@AssertTrue`-ed methods ? Some naming convention holds ? – Stephane Nov 20 '15 at 11:35
  • @Alberthoven How to show the error message as its not against any field. – Zahid Khan Mar 24 '16 at 11:08
  • When i use @AssertTrue my method is called multiple times... Can anyone help me? [link](https://stackoverflow.com/questions/45989381/when-using-asserttrue-in-methods-the-method-is-invoked-4-times-during-validati) – Henrique Fernandes Cipriano Aug 31 '17 at 21:37
  • 6
    @Hardy Whilst some may view this as a mixing of concerns, it could be argued that JSR 303 encourages violation of the more fundamental principle of encapsulation. Why shouldn't a domain object know how to validate its own private state? I think the currently prevalent view is just a reflection of the extent to which JPA and Hibernate have encouraged the wide spread of the Anaemic Domain anti-pattern. – Dave Jan 08 '18 at 02:39
  • 1
    This is the best answer. Honestly, I think that declarative validation makes things much more difficult. It's easy enough to validate individual properties in a setter, and cross-property validation can be done in a single validate() method. – Greg Brown Aug 16 '18 at 14:48
  • @bhantol - you can't externalize logic stored in a custom validator, either. – Greg Brown Aug 16 '18 at 14:49
  • @GregBrown not the logic but the declarative properties as annotated can be done using a custom validator. Here is a lame years old way https://github.com/yogeshgadge/crossfield-bean-validator/blob/master/crossfieldvalidator/src/main/java/org/beanvalidator/customvalidator/examples/UserBeanValidatorTester.java where a field described which `OtherField` it is dependent on but it can be more descriptive and the entire logic can be described. – bhantol Aug 16 '18 at 16:46
  • https://github.com/FasterXML/jackson-module-jsonSchema can externalize the JSON which some other platform (e.e. Javascript) can interpret and apply the logic. But you are right the logic itself cannot be externalized. – bhantol Aug 16 '18 at 16:53
  • 4
    @Stephane I'm coding all such cross-field validations in different methods, in order to have appropriate error messages. They must all begin with `is`, otherwise the framework will ignore them. The actual name matters only to you, since the methods are best left as `private`. For example: `@AssertTrue(message="The two email fields should be the same") private boolean isEmailVerifyValid() { return this.email.equals(this.emailVerify); }` and `@AssertTrue(message="The two password fields should be the same") private boolean isPassVerifyValid() { return this.pass.equals(this.passVerify); }` – Tobia Apr 07 '19 at 13:49
  • @funky-nd missing real fields and respective values – yongfa365 Nov 12 '19 at 09:41
  • 1
    The method name should be `isFieldName` where `FieldName` matches the field that is being validated. E.g. if you have a field `firstName` you would validate it with `@AssertTrue private boolean isFirstName() { ... }` -- this allows Spring to match the validation result with the field it's related to. – Josh M. Jun 10 '20 at 21:41
35

I'm surprised this isn't available out of the box. Anyway, here is a possible solution.

I've created a class level validator, not the field level as described in the original question.

Here is the annotation code:

package com.moa.podium.util.constraints;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

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

  String message() default "{com.moa.podium.util.constraints.matches}";

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

  Class<? extends Payload>[] payload() default {};

  String field();

  String verifyField();
}

And the validator itself:

package com.moa.podium.util.constraints;

import org.mvel2.MVEL;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class MatchesValidator implements ConstraintValidator<Matches, Object> {

  private String field;
  private String verifyField;


  public void initialize(Matches constraintAnnotation) {
    this.field = constraintAnnotation.field();
    this.verifyField = constraintAnnotation.verifyField();
  }

  public boolean isValid(Object value, ConstraintValidatorContext context) {
    Object fieldObj = MVEL.getProperty(field, value);
    Object verifyFieldObj = MVEL.getProperty(verifyField, value);

    boolean neitherSet = (fieldObj == null) && (verifyFieldObj == null);

    if (neitherSet) {
      return true;
    }

    boolean matches = (fieldObj != null) && fieldObj.equals(verifyFieldObj);

    if (!matches) {
      context.disableDefaultConstraintViolation();
      context.buildConstraintViolationWithTemplate("message")
          .addNode(verifyField)
          .addConstraintViolation();
    }

    return matches;
  }
}

Note that I've used MVEL to inspect the properties of the object being validated. This could be replaced with the standard reflection APIs or if it is a specific class you are validating, the accessor methods themselves.

The @Matches annotation can then be used used on a bean as follows:

@Matches(field="pass", verifyField="passRepeat")
public class AccountCreateForm {

  @Size(min=6, max=50)
  private String pass;
  private String passRepeat;

  ...
}

As a disclaimer, I wrote this in the last 5 minutes, so I probably haven't ironed out all the bugs yet. I'll update the answer if anything goes wrong.

Geinmachi
  • 1,251
  • 1
  • 8
  • 20
Bradley Dwyer
  • 8,102
  • 5
  • 32
  • 28
  • 1
    This is great and it's working for me, except that addNote is deprecated and I get AbstractMethodError if I use addPropertyNode instead. Google isn't helping me here. What the solution? Is there a dependency missing somewhere? – Paul Grenyer Dec 01 '13 at 15:37
30

With Hibernate Validator 4.1.0.Final I recommend using @ScriptAssert. 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.

Note: the evaluation is being performed by a scripting "engine" running in the Java VM, therefore on Java "server side", not on "client side" as stated in some comments.

Example:

@ScriptAssert(lang = "javascript", script = "_this.passVerify.equals(_this.pass)")
public class MyBean {
  @Size(min=6, max=50)
  private String pass;

  private String passVerify;
}

or with shorter alias and null-safe:

@ScriptAssert(lang = "javascript", alias = "_",
    script = "_.passVerify != null && _.passVerify.equals(_.pass)")
public class MyBean {
  @Size(min=6, max=50)
  private String pass;

  private String passVerify;
}

or with Java 7+ null-safe Objects.equals():

@ScriptAssert(lang = "javascript", script = "Objects.equals(_this.passVerify, _this.pass)")
public class MyBean {
  @Size(min=6, max=50)
  private String pass;

  private String passVerify;
}

Nevertheless, there is nothing wrong with a custom class level validator @Matches solution.

t0r0X
  • 4,212
  • 1
  • 38
  • 34
Hardy
  • 18,659
  • 3
  • 49
  • 65
  • 2
    Interesting solution, are we really employing javascript here to accomplish this validation? That seems like overkill for what a java based annotation should be able to accomplish. To my virgin eyes the solution by Nicko proposed above still seems cleaner both from a usability standpoint (his annotation is easy to read and quite functional vs. inelegant javascript->java references), and from a scalability standpoint (I assume there's reasonable overhead to handle the javascript, but maybe Hibernate is caching the compiled code at least?). I'm curious to understand why this would be preferred. – David Parks Nov 08 '10 at 13:30
  • 3
    I agree that Nicko's implementation is nice, but I don't see anything objectionable about using JS as an expression language. Java 6 includes Rhino for exactly such applications. I like @ScriptAssert as it just works without my having to create an annotation and a validator every time I have a novel type of test to perform. –  Apr 06 '11 at 07:30
  • 4
    As said, nothing is wrong with the class level validator. ScriptAssert is just an alternative which does not require you to write custom code. I did not say that it is the preferred solution ;-) – Hardy Apr 06 '11 at 14:34
  • Great answer because password confirmation is not critical validation therefore it can be done on the client side – Peter Chaula Dec 30 '18 at 17:57
  • I have searched high and low for examples of how to set up `@ScriptAssert` in a project so that the language is found. No matter what I put from what example (spring, javascript) , there is always an error saying that the language cannot be found. And so far, I have been unable to find any documentation of how to set up one of those language. I would assume that "spring" is supposed to work out-of-the-box with Spring Boot, but alas it does not. – GreenSaguaro Sep 17 '21 at 04:25
20

Cross fields validations can be done by creating custom constraints.

Example:- Compare password and confirmPassword fields of User instance.

CompareStrings

@Target({TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy=CompareStringsValidator.class)
@Documented
public @interface CompareStrings {
    String[] propertyNames();
    StringComparisonMode matchMode() default EQUAL;
    boolean allowNull() default false;
    String message() default "";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

StringComparisonMode

public enum StringComparisonMode {
    EQUAL, EQUAL_IGNORE_CASE, NOT_EQUAL, NOT_EQUAL_IGNORE_CASE
}

CompareStringsValidator

public class CompareStringsValidator implements ConstraintValidator<CompareStrings, Object> {

    private String[] propertyNames;
    private StringComparisonMode comparisonMode;
    private boolean allowNull;

    @Override
    public void initialize(CompareStrings constraintAnnotation) {
        this.propertyNames = constraintAnnotation.propertyNames();
        this.comparisonMode = constraintAnnotation.matchMode();
        this.allowNull = constraintAnnotation.allowNull();
    }

    @Override
    public boolean isValid(Object target, ConstraintValidatorContext context) {
        boolean isValid = true;
        List<String> propertyValues = new ArrayList<String> (propertyNames.length);
        for(int i=0; i<propertyNames.length; i++) {
            String propertyValue = ConstraintValidatorHelper.getPropertyValue(String.class, propertyNames[i], target);
            if(propertyValue == null) {
                if(!allowNull) {
                    isValid = false;
                    break;
                }
            } else {
                propertyValues.add(propertyValue);
            }
        }

        if(isValid) {
            isValid = ConstraintValidatorHelper.isValid(propertyValues, comparisonMode);
        }

        if (!isValid) {
          /*
           * if custom message was provided, don't touch it, otherwise build the
           * default message
           */
          String message = context.getDefaultConstraintMessageTemplate();
          message = (message.isEmpty()) ?  ConstraintValidatorHelper.resolveMessage(propertyNames, comparisonMode) : message;

          context.disableDefaultConstraintViolation();
          ConstraintViolationBuilder violationBuilder = context.buildConstraintViolationWithTemplate(message);
          for (String propertyName : propertyNames) {
            NodeBuilderDefinedContext nbdc = violationBuilder.addNode(propertyName);
            nbdc.addConstraintViolation();
          }
        }    

        return isValid;
    }
}

ConstraintValidatorHelper

public abstract class ConstraintValidatorHelper {

public static <T> T getPropertyValue(Class<T> requiredType, String propertyName, Object instance) {
        if(requiredType == null) {
            throw new IllegalArgumentException("Invalid argument. requiredType must NOT be null!");
        }
        if(propertyName == null) {
            throw new IllegalArgumentException("Invalid argument. PropertyName must NOT be null!");
        }
        if(instance == null) {
            throw new IllegalArgumentException("Invalid argument. Object instance must NOT be null!");
        }
        T returnValue = null;
        try {
            PropertyDescriptor descriptor = new PropertyDescriptor(propertyName, instance.getClass());
            Method readMethod = descriptor.getReadMethod();
            if(readMethod == null) {
                throw new IllegalStateException("Property '" + propertyName + "' of " + instance.getClass().getName() + " is NOT readable!");
            }
            if(requiredType.isAssignableFrom(readMethod.getReturnType())) {
                try {
                    Object propertyValue = readMethod.invoke(instance);
                    returnValue = requiredType.cast(propertyValue);
                } catch (Exception e) {
                    e.printStackTrace(); // unable to invoke readMethod
                }
            }
        } catch (IntrospectionException e) {
            throw new IllegalArgumentException("Property '" + propertyName + "' is NOT defined in " + instance.getClass().getName() + "!", e);
        }
        return returnValue; 
    }

    public static boolean isValid(Collection<String> propertyValues, StringComparisonMode comparisonMode) {
        boolean ignoreCase = false;
        switch (comparisonMode) {
        case EQUAL_IGNORE_CASE:
        case NOT_EQUAL_IGNORE_CASE:
            ignoreCase = true;
        }

        List<String> values = new ArrayList<String> (propertyValues.size());
        for(String propertyValue : propertyValues) {
            if(ignoreCase) {
                values.add(propertyValue.toLowerCase());
            } else {
                values.add(propertyValue);
            }
        }

        switch (comparisonMode) {
        case EQUAL:
        case EQUAL_IGNORE_CASE:
            Set<String> uniqueValues = new HashSet<String> (values);
            return uniqueValues.size() == 1 ? true : false;
        case NOT_EQUAL:
        case NOT_EQUAL_IGNORE_CASE:
            Set<String> allValues = new HashSet<String> (values);
            return allValues.size() == values.size() ? true : false;
        }

        return true;
    }

    public static String resolveMessage(String[] propertyNames, StringComparisonMode comparisonMode) {
        StringBuffer buffer = concatPropertyNames(propertyNames);
        buffer.append(" must");
        switch(comparisonMode) {
        case EQUAL:
        case EQUAL_IGNORE_CASE:
            buffer.append(" be equal");
            break;
        case NOT_EQUAL:
        case NOT_EQUAL_IGNORE_CASE:
            buffer.append(" not be equal");
            break;
        }
        buffer.append('.');
        return buffer.toString();
    }

    private static StringBuffer concatPropertyNames(String[] propertyNames) {
        //TODO improve concating algorithm
        StringBuffer buffer = new StringBuffer();
        buffer.append('[');
        for(String propertyName : propertyNames) {
            char firstChar = Character.toUpperCase(propertyName.charAt(0));
            buffer.append(firstChar);
            buffer.append(propertyName.substring(1));
            buffer.append(", ");
        }
        buffer.delete(buffer.length()-2, buffer.length());
        buffer.append("]");
        return buffer;
    }
}

User

@CompareStrings(propertyNames={"password", "confirmPassword"})
public class User {
    private String password;
    private String confirmPassword;

    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }
    public String getConfirmPassword() { return confirmPassword; }
    public void setConfirmPassword(String confirmPassword) { this.confirmPassword =  confirmPassword; }
}

Test

    public void test() {
        User user = new User();
        user.setPassword("password");
        user.setConfirmPassword("paSSword");
        Set<ConstraintViolation<User>> violations = beanValidator.validate(user);
        for(ConstraintViolation<User> violation : violations) {
            logger.debug("Message:- " + violation.getMessage());
        }
        Assert.assertEquals(violations.size(), 1);
    }

Output Message:- [Password, ConfirmPassword] must be equal.

By using the CompareStrings validation constraint, we can also compare more than two properties and we can mix any of four string comparison methods.

ColorChoice

@CompareStrings(propertyNames={"color1", "color2", "color3"}, matchMode=StringComparisonMode.NOT_EQUAL, message="Please choose three different colors.")
public class ColorChoice {

    private String color1;
    private String color2;
    private String color3;
        ......
}

Test

ColorChoice colorChoice = new ColorChoice();
        colorChoice.setColor1("black");
        colorChoice.setColor2("white");
        colorChoice.setColor3("white");
        Set<ConstraintViolation<ColorChoice>> colorChoiceviolations = beanValidator.validate(colorChoice);
        for(ConstraintViolation<ColorChoice> violation : colorChoiceviolations) {
            logger.debug("Message:- " + violation.getMessage());
        }

Output Message:- Please choose three different colors.

Similarly, we can have CompareNumbers, CompareDates, etc cross-fields validation constraints.

P.S. I have not tested this code under production environment (though I tested it under dev environment), so consider this code as Milestone Release. If you find a bug, please write a nice comment. :)

Aserre
  • 4,916
  • 5
  • 33
  • 56
dira
  • 30,304
  • 14
  • 54
  • 69
  • I like this approach, as it is more flexible than the others. It lets me validate more than 2 fields for equality. Nice job! – Tauren Feb 14 '11 at 23:29
14

If you’re using the Spring Framework then you can use the Spring Expression Language (SpEL) for that. I’ve wrote a small library that provides JSR-303 validator based on SpEL – it makes cross-field validations a breeze! Take a look at https://github.com/jirutka/validator-spring.

This will validate length and equality of the password fields.

@SpELAssert(value = "pass.equals(passVerify)",
            message = "{validator.passwords_not_same}")
public class MyBean {

    @Size(min = 6, max = 50)
    private String pass;

    private String passVerify;
}

You can also easily modify this to validate the password fields only when not both empty.

@SpELAssert(value = "pass.equals(passVerify)",
            applyIf = "pass || passVerify",
            message = "{validator.passwords_not_same}")
public class MyBean {

    @Size(min = 6, max = 50)
    private String pass;

    private String passVerify;
}
Aserre
  • 4,916
  • 5
  • 33
  • 56
Jakub Jirutka
  • 10,269
  • 4
  • 42
  • 35
10

I have tried Alberthoven's example (hibernate-validator 4.0.2.GA) and i get an ValidationException: „Annotated methods must follow the JavaBeans naming convention. match() does not.“ too. After I renamed the method from „match“ to "isValid" it works.

public class Password {

    private String password;

    private String retypedPassword;

    public Password(String password, String retypedPassword) {
        super();
        this.password = password;
        this.retypedPassword = retypedPassword;
    }

    @AssertTrue(message="password should match retyped password")
    private boolean isValid(){
        if (password == null) {
            return retypedPassword == null;
        } else {
            return password.equals(retypedPassword);
        }
    }

    public String getPassword() {
        return password;
    }

    public String getRetypedPassword() {
        return retypedPassword;
    }

}
Aserre
  • 4,916
  • 5
  • 33
  • 56
Ralph
  • 118,862
  • 56
  • 287
  • 383
  • It worked correctly for me but didn't display the error message. Did it work and display the error message for you. How? – Tiny Aug 10 '12 at 03:44
  • 1
    @Tiny: The message should be in the violations returned by the validator. (Write a Unit test: http://stackoverflow.com/questions/5704743/unit-testing-jsr-303-validation-in-spring). BUT the validation message belongs to the "isValid" Property. Therefore the message will be only shown in the GUI if the GUI show the problems for retypedPassword AND isValid (next to retyped Password). – Ralph Aug 10 '12 at 07:03
6

I like the idea from Jakub Jirutka to use Spring Expression Language. If you don't want to add another library/dependency (assuming that you already use Spring), here is a simplified implementation of his idea.

The constraint:

@Constraint(validatedBy=ExpressionAssertValidator.class)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExpressionAssert {
    String message() default "expression must evaluate to true";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    String value();
}

The validator:

public class ExpressionAssertValidator implements ConstraintValidator<ExpressionAssert, Object> {
    private Expression exp;

    public void initialize(ExpressionAssert annotation) {
        ExpressionParser parser = new SpelExpressionParser();
        exp = parser.parseExpression(annotation.value());
    }

    public boolean isValid(Object value, ConstraintValidatorContext context) {
        return exp.getValue(value, Boolean.class);
    }
}

Apply like this:

@ExpressionAssert(value="pass == passVerify", message="passwords must be same")
public class MyBean {
    @Size(min=6, max=50)
    private String pass;
    private String passVerify;
}
holmis83
  • 15,922
  • 5
  • 82
  • 83
4

I made a small adaptation in Nicko's solution so that it is not necessary to use the Apache Commons BeanUtils library and replace it with the solution already available in spring, for those using it as I can be simpler:

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object> {

    private String firstFieldName;
    private String secondFieldName;

    @Override
    public void initialize(final FieldMatch constraintAnnotation) {
        firstFieldName = constraintAnnotation.first();
        secondFieldName = constraintAnnotation.second();
    }

    @Override
    public boolean isValid(final Object object, final ConstraintValidatorContext context) {

        BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(object);
        final Object firstObj = beanWrapper.getPropertyValue(firstFieldName);
        final Object secondObj = beanWrapper.getPropertyValue(secondFieldName);

        boolean isValid = firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);

        if (!isValid) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
                .addPropertyNode(firstFieldName)
                .addConstraintViolation();
        }

        return isValid;

    }
}
Pedro Bacchini
  • 876
  • 10
  • 13
3

I don't have the reputation for commenting on the first answer but wanted to add that I have added unit tests for the winning answer and have the following observations:

  • If you get the first or field names wrong then you get a validation error as though the values don't match. Don't get tripped up by spelling mistakes e.g.

@FieldMatch(first="invalidFieldName1", second="validFieldName2")

  • The validator will accept equivalent data types i.e. these will all pass with FieldMatch:

private String stringField = "1";

private Integer integerField = new Integer(1)

private int intField = 1;

  • If the fields are of an object type which does not implement equals, the validation will fail.
Community
  • 1
  • 1
Chanoch
  • 563
  • 7
  • 16
2

Very nice solution bradhouse. Is there any way to apply the @Matches annotation to more than one field?

EDIT: Here's the solution I came up with to answer this question, I modified the Constraint to accept an array instead of a single value:

@Matches(fields={"password", "email"}, verifyFields={"confirmPassword", "confirmEmail"})
public class UserRegistrationForm  {

    @NotNull
    @Size(min=8, max=25)
    private String password;

    @NotNull
    @Size(min=8, max=25)
    private String confirmPassword;


    @NotNull
    @Email
    private String email;

    @NotNull
    @Email
    private String confirmEmail;
}

The code for the annotation:

package springapp.util.constraints;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

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

  String message() default "{springapp.util.constraints.matches}";

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

  Class<? extends Payload>[] payload() default {};

  String[] fields();

  String[] verifyFields();
}

And the implementation:

package springapp.util.constraints;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import org.apache.commons.beanutils.BeanUtils;

public class MatchesValidator implements ConstraintValidator<Matches, Object> {

    private String[] fields;
    private String[] verifyFields;

    public void initialize(Matches constraintAnnotation) {
        fields = constraintAnnotation.fields();
        verifyFields = constraintAnnotation.verifyFields();
    }

    public boolean isValid(Object value, ConstraintValidatorContext context) {

        boolean matches = true;

        for (int i=0; i<fields.length; i++) {
            Object fieldObj, verifyFieldObj;
            try {
                fieldObj = BeanUtils.getProperty(value, fields[i]);
                verifyFieldObj = BeanUtils.getProperty(value, verifyFields[i]);
            } catch (Exception e) {
                //ignore
                continue;
            }
            boolean neitherSet = (fieldObj == null) && (verifyFieldObj == null);
            if (neitherSet) {
                continue;
            }

            boolean tempMatches = (fieldObj != null) && fieldObj.equals(verifyFieldObj);

            if (!tempMatches) {
                addConstraintViolation(context, fields[i]+ " fields do not match", verifyFields[i]);
            }

            matches = matches?tempMatches:matches;
        }
        return matches;
    }

    private void addConstraintViolation(ConstraintValidatorContext context, String message, String field) {
        context.disableDefaultConstraintViolation();
        context.buildConstraintViolationWithTemplate(message).addNode(field).addConstraintViolation();
    }
}
Aserre
  • 4,916
  • 5
  • 33
  • 56
McGin
  • 1,361
  • 2
  • 13
  • 31
  • Hmm. Not sure. You could try either creating specific validators for each confirmation field (so they have different annotations), or updating the @Matches annotation to accept multiple pairs of fields. – Bradley Dwyer Jan 20 '10 at 20:54
  • Thanks bradhouse, came up with a solution and have posted it above. It needs a little work to cater for when differing number of arguements are passed so you don't get IndexOutOfBoundsExceptions, but the basics are there. – McGin Jan 22 '10 at 13:32
1

You need to call it explicitly. In the example above, bradhouse has given you all the steps to write a custom constraint.

Add this code in your caller class.

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();

Set<ConstraintViolation<yourObjectClass>> constraintViolations = validator.validate(yourObject);

in the above case it would be

Set<ConstraintViolation<AccountCreateForm>> constraintViolations = validator.validate(objAccountCreateForm);
Clive
  • 36,918
  • 8
  • 87
  • 113
Vishal
  • 11
  • 1
1

You guys are awesome. Really amazing ideas. I like Alberthoven's and McGin's most, so I decided to combine both ideas. And develop some generic solution to cater all cases. Here is my proposed solution.

@Documented
@Constraint(validatedBy = NotFalseValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotFalse {


    String message() default "NotFalse";
    String[] messages();
    String[] properties();
    String[] verifiers();

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

    Class<? extends Payload>[] payload() default {};

}

public class NotFalseValidator implements ConstraintValidator<NotFalse, Object> {
    private String[] properties;
    private String[] messages;
    private String[] verifiers;
    @Override
    public void initialize(NotFalse flag) {
        properties = flag.properties();
        messages = flag.messages();
        verifiers = flag.verifiers();
    }

    @Override
    public boolean isValid(Object bean, ConstraintValidatorContext cxt) {
        if(bean == null) {
            return true;
        }

        boolean valid = true;
        BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean);

        for(int i = 0; i< properties.length; i++) {
           Boolean verified = (Boolean) beanWrapper.getPropertyValue(verifiers[i]);
           valid &= isValidProperty(verified,messages[i],properties[i],cxt);
        }

        return valid;
    }

    boolean isValidProperty(Boolean flag,String message, String property, ConstraintValidatorContext cxt) {
        if(flag == null || flag) {
            return true;
        } else {
            cxt.disableDefaultConstraintViolation();
            cxt.buildConstraintViolationWithTemplate(message)
                    .addPropertyNode(property)
                    .addConstraintViolation();
            return false;
        }

    }



}

@NotFalse(
        messages = {"End Date Before Start Date" , "Start Date Before End Date" } ,
        properties={"endDateTime" , "startDateTime"},
        verifiers = {"validDateRange" , "validDateRange"})
public class SyncSessionDTO implements ControllableNode {
    @NotEmpty @NotPastDate
    private Date startDateTime;

    @NotEmpty
    private Date endDateTime;



    public Date getStartDateTime() {
        return startDateTime;
    }

    public void setStartDateTime(Date startDateTime) {
        this.startDateTime = startDateTime;
    }

    public Date getEndDateTime() {
        return endDateTime;
    }

    public void setEndDateTime(Date endDateTime) {
        this.endDateTime = endDateTime;
    }


    public Boolean getValidDateRange(){
        if(startDateTime != null && endDateTime != null) {
            return startDateTime.getTime() <= endDateTime.getTime();
        }

        return null;
    }

}
Sheikh Abdul Wahid
  • 2,623
  • 2
  • 25
  • 24
1

Why not try Oval: http://oval.sourceforge.net/

I looks like it supports OGNL so maybe you could do it by a more natural

@Assert(expr = "_value ==_this.pass").
stema
  • 90,351
  • 20
  • 107
  • 135
Mircea D.
  • 331
  • 4
  • 14
-1

Solution realated with question: How to access a field which is described in annotation property

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Match {

    String field();

    String message() default "";
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MatchValidator.class)
@Documented
public @interface EnableMatchConstraint {

    String message() default "Fields must match!";

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

    Class<? extends Payload>[] payload() default {};
}

public class MatchValidator implements  ConstraintValidator<EnableMatchConstraint, Object> {

    @Override
    public void initialize(final EnableMatchConstraint constraint) {}

    @Override
    public boolean isValid(final Object o, final ConstraintValidatorContext context) {
        boolean result = true;
        try {
            String mainField, secondField, message;
            Object firstObj, secondObj;

            final Class<?> clazz = o.getClass();
            final Field[] fields = clazz.getDeclaredFields();

            for (Field field : fields) {
                if (field.isAnnotationPresent(Match.class)) {
                    mainField = field.getName();
                    secondField = field.getAnnotation(Match.class).field();
                    message = field.getAnnotation(Match.class).message();

                    if (message == null || "".equals(message))
                        message = "Fields " + mainField + " and " + secondField + " must match!";

                    firstObj = BeanUtils.getProperty(o, mainField);
                    secondObj = BeanUtils.getProperty(o, secondField);

                    result = firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
                    if (!result) {
                        context.disableDefaultConstraintViolation();
                        context.buildConstraintViolationWithTemplate(message).addPropertyNode(mainField).addConstraintViolation();
                        break;
                    }
                }
            }
        } catch (final Exception e) {
            // ignore
            //e.printStackTrace();
        }
        return result;
    }
}

And how to use it...? Like this:

@Entity
@EnableMatchConstraint
public class User {

    @NotBlank
    private String password;

    @Match(field = "password")
    private String passwordConfirmation;
}
kryski
  • 414
  • 2
  • 6
  • 18