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
Spring Boot JPA - Onetomany Relationship Causes Infinite Loop
Javafx Column in Tableview Auto Fit Size
Crudrepository and Hibernate: Save(List<S>) VS Save(Entity) in Transaction
Spring MVC - Get Httpservletresponse Body
Spring Boot @Enablescheduling Conditionally
Value Annotation Not Working in Junit Test
How to Junit Test That Two List<E> Contain the Same Elements in the Same Order
Spring Boot Controller Not Mapping
How to Apply Spring Boot Filter Based on Url Pattern
How to Refresh a Stale Selenium Element If I Don't Have the Original Xpath
Intellij Compilation Error Zip End Header Not Found
Using Comparable to Compare Generic Variables
Simpledateformat Producing Wrong Date Time When Parsing "Yyyy-Mm-Dd Hh:Mm"
How to Avoid 302 Response on Https Spring Security Unit Test
Sending Variable from Adapter to Activity
Adb Cannot Connect to Daemon At Tcp:5037
Create List of Object from Another Using Java 8 Streams
Spring-Data-Jpa Repository - Underscore on Entity Column Name