Jsr 303 Validation, If One Field Equals "Something", Then These Other Fields Should Not Be Null

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

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)

Spring JSR 303 Validation access other field value while Edit/Add

I came with a work around by annotating on a getter method where all the required fields are returned as a single map through that method and in the validationIMPL I retrieved all the required information and processed accordingly.

 private String roleName;

@UniqueValue(query = AppConstants.UNIQUE_VALIDATION_DB_QUERY)
public Map<String,String> getUniqueValidator(){
Map<String,String> validatorMap=new HashMap<String,String>();

validatorMap.put("ACTION",type of action(update/new)):
validatorMap.put("VALUE",this.roleName):
return validatorMap;
}

public String getRoleName() {
return roleName;
}

public void setRoleName(String roleName) {
this.roleName = roleName;
}

Cross field validation with Hibernate Validator (JSR 303)

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;
}
}

JSR-303: Yet Another Cross-field Validation Problem

IMO, the simplest solution is to create a separate java class say Money that holds both information, the type of money (i.e. Currency) and the value of money.

public class Money {

private Currency currency;
private Double value;

public Currency getCurrency() { return currency; }
public void setCurrency(Currency currency) { this.currency = currency; }

public Double getValue() { return value; }
public void setValue(Double value) { this.value = value; }

public boolean isValid() {
if(getCurrency() == null || getValue() == null) {
return false;
}

// critical logic goes here

// sample code
if("JPY".equalsIgnoreCase(currency.getCurrencyCode())) {
int intValue = getValue().intValue();
double diff = getValue() - intValue;
if(diff > 0) {
return false;
}
}

/*double fractionValue = value - (value % (currency.getDefaultFractionDigits() * 10));
if(fractionValue > currency.getDefaultFractionDigits() * 10) {
return false;
}*/

return true;
}

}

After this, create a constraint say @ValidMoney and MoneyValidator.

public class MoneyValidator implements ConstraintValidator<ValidMoney, Money> {

@Override
public void initialize(ValidMoney constraintAnnotation) {
// TODO Auto-generated method stub
}

@Override
public boolean isValid(Money value, ConstraintValidatorContext context) {
return value.isValid();
}

}

Example:-

public class Bid {
@ValidMoney
private Money bidAmount;
}

Cross field validation (JSR 303) problem

You could do this by annotating MyBean with a custom validator, for example:

@ValidMyBean
public class MyBean {

private boolean selected;

private String someString;

...
}

ValidMyBean:

@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MyBeanValidator.class)
public @interface ValidMyBean {

boolean allViolationMessages() default true;

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

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

String message() default "{ValidMyBean.message}";

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

MyBeanValidator:

public final class MyBeanValidator implements
ConstraintValidator<ValidMyBean, MyBean> {

@Override
public void initialize(
@SuppressWarnings("unused") final ValidMyBean constraintAnnotation) {
}

@Override
public boolean isValid(final MyBean value,
final ConstraintValidatorContext context) {

boolean isValid = true;
//your validation here

return isValid;
}
}


Related Topics



Leave a reply



Submit