Generic inter-field bean validator using SpEl
What is it ?
It’s a generic validator by using spring exprssion language to specify contraints in Spring code. Improves readability by keeping validation logic in same POJO/DTO . It’s a type(Class) level validator where inter field constraints can be specified easily by SpEl.
Known benefits:
- Avoid writting custom validator for diffrent beans.
- High readability by keeping constraints in same bean class.
- Allow to express validation logic by SpEl (Spring expression language)
Technical background
Brief hisroty of validator in Java world
J2EE(Spring) is well familiar with POJO/DTO validator for long, it’s well organized, standadised by java community and used by most. It’s JSR(Java Specification Requests)-380 and implemented and maintained by hibernate community. Spring supports JSR-380 inherently.
What’s addressed by JSR-380
JSR-380 requirements is captured in below artifact
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
It came up with an extendiable frameowkr which allows user to implement their requirements when default validators does not support so user can extends it.
These are validators specified in javax.validator.validation-api
| | | | |
|—————- | —————– | ————- | ————– |
| AssertFalse | Future | NotBlank | Pattern |
| AssertTrue | FutureOrPresent | NotEmpty | Positive |
| DecimalMax | Max | NotNull | PositiveOrZero |
| DecimalMin | Min | Null | Size |
| Digits | Negative | Past | |
| Email | NegativeOrZero | PastOrPresent | |
It’s implemented by hibernate community and being used by spring
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
first contribution
Nature of all validator is either at class level or field level but does not address the inter field validator in a bean
Second contribution
Framework to extend the validation for developer using given interfaces and classes in javax.validator
.
What is tried to bring
What is cross field validation ?
Below is a sample class which as few field. But suppose there is a requirements as below Validation requirements
name
should be not empty string.surname
should be non-empty for PERSON and empty for ORGANIZATION customerType.dob
should be non-empty for PERSON customerType but for ORGANIZATION should be empty.doi
should be empty for PERSON but non-empty for ORGANIZATION customerType.addresses
should be size of one for ORGANIZATION and can be two but at least one for PERSON customerType.customerType
should not be empty and one of the value of enum CustomerType.java
Below a try to impose those validation using javax.validator
. Now we can see main issue is the conditional validation requirements which here mainly
depends on customerType
.
public enum CustomerType {
PERSON, ORGANIZATION
}
public class CustomerDTO {
@NotEmpty(groups = PostMapping.class)
private String name;
@NotEmpty(groups = PostMapping.class)
private String surname;
@NotEmpty(groups = PostMapping.class)
private LocalDate dob;
@NotEmpty(groups = PostMapping.class)
private LocalDate doi;
@Size(min=1, max=2)
private List<@Valid AddressDTO> addresses;
@NotEmpty(groups = PostMapping.class)
private CustomerType customerType;
}
It’s not possible to enforce the restriction using default set of validator hence developer will opt for custom validator for CustomerDTO
or write logic in service layer.
Below is the way explained how it can done by custom validator.
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = { CustomerValidatorImpl.class })
@Documented
public @interface CustomerValidator {
String message() default "failed!";
Class<?>[] groups() default { };
Class<? extends Payload>[]payload() default {};
SpElCrossFieldCondition[] conditions();
}
public class CustomerValidatorImpl implements ConstraintValidator<CrossFieldValidator, CustomerDTO> {
@Override
public boolean isValid(CustomerDTO customerDTO, ConstraintValidatorContext context) {
return (custmerDTO.getCustomerType() == ORGANIZATION
&& StringUtils.isEmpty(custmerDTO.getSurname())
&& Objects.isNull(custmerDTO.getDOB())
&& Objects.nonNull(custmerDTO.getDOI())
&& Objects.nonNull(addresses) && addresses.size() == 1)
|| (custmerDTO.getCustomerType() == PERSON
&& !StringUtils.isEmpty(custmerDTO.getSurname())
&& Objects.isNull(custmerDTO.getDOI())
&& Objects.nonNull(custmerDTO.getDOB())
&& Objects.nonNull(addresses) && addresses.size() >= 1 && addresses.size() <= 2);
}
}
But what is there is conditional cross-field validation required for AddressDTO
nested object. In that case is current approach we have to write an another custom validator
related to AddressDTO.
What is sorted here to address above issue ?
What if we just can declare our cross-field conditional restriction using annotation at POJO/DTO class as below
@Data
@CrossFieldValidator(groups = {PostMapping.class}, conditions = {
@SpElCrossFieldCondition(IF = "customerType == T(com.sf.customvalidator.constant.CustomerType).ORGANIZATION",
THEN = "surname==null"),
@SpElCrossFieldCondition(IF = "customerType == T(com.sf.customvalidator.constant.CustomerType).ORGANIZATION",
THEN = "dob == null AND doi != null"),
@SpElCrossFieldCondition(IF = "customerType == T(com.sf.customvalidator.constant.CustomerType).ORGANIZATION",
THEN = "addresses!=null AND addresses.size() == 1"),
@SpElCrossFieldCondition(IF = "customerType == T(com.sf.customvalidator.constant.CustomerType).PERSON",
THEN = "surname!=null AND !surname.isEmpty()"),
@SpElCrossFieldCondition(IF = "customerType == T(com.sf.customvalidator.constant.CustomerType).PERSON",
THEN = "dob != null AND doi == null"),
@SpElCrossFieldCondition(IF = "customerType == T(com.sf.customvalidator.constant.CustomerType).PERSON",
THEN = "addresses!=null AND addresses.size() >= 1 AND addresses.size() <= 2")
})
public class CustomerDTO {
@NotEmpty(groups = PostMapping.class)
private String name;
private String surname;
private LocalDate dob;
private LocalDate doi;
private List<@Valid AddressDTO> addresses;
@NotEmpty(groups = PostMapping.class)
private CustomerType customerType;
}
@Data
@CrossFieldValidator(groups = {PostMapping.class}, conditions = {
@SpElCrossFieldCondition(IF = "country == T(com.sf.customvalidator.constant.Country).US OR country == T(com.sf.customvalidator.constant.Country).DE", THEN = "areaCode != null && areaCode.length == 5"),
@SpElCrossFieldCondition(IF = "country == T(com.sf.customvalidator.constant.Country).IND", THEN = "areaCode != null && areaCode.length == 6")
})
public class AddressDTO {
@NotEmpty(groups = {PostMapping.class})
private String houseNo;
@NotEmpty(groups = {PostMapping.class})
private String lane;
@NotEmpty(groups = {PostMapping.class})
private Country country;
@NotEmpty(groups = {PostMapping.class})
private String areaCode;
@NotEmpty(groups = {PostMapping.class})
private String state;
}
What we just achieve by above annotation approach
- generic code so no need to write logic using verbose language and framework syntax
- higher code readability as validation is written upfront on DTO
How to write condition for the validation ?
We have to use our familiar spring expression language (SpEl) to expression our IF
and THEN
condition, Documentation of spring expression langauge can be found in
SpEl
Implementation of generic validater is here
SpEl Cross field validator you can have a look at implemenation I am here to talk about how it should be used rather internal details as those are not very interesting. CrossFieldValidatorImpl.java