I am validating REST service request/bean in a spring-boot 2.3.1.RELEASE web application. Currently, I am using Hibernate Validator, though I am open to using any other way for validation.
Say, I have a model Foo
, which I receive as a request in a Rest Controller
. And I want to validate if completionDate
is not null
then status
should be either "complete" or "closed".
@StatusValidate
public class Foo {
private String status;
private LocalDate completionDate;
// getters and setters
}
I created a custom class level annotation @StatusValidate
.
@Constraint(validatedBy = StatusValidator.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface StatusValidate {
String message() default "default status error";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
I created StatusValidator
class.
public class StatusValidator implements ConstraintValidator<StatusValidate, Foo> {
@Override
public void initialize(StatusValidateconstraintAnnotation) {
}
@Override
public boolean isValid(Foovalue, ConstraintValidatorContext context) {
if (null != value.getCompletionDate() && (!value.getStatus().equalsIgnoreCase("complete") && !value.getStatus().equalsIgnoreCase("closed"))) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()).
.addPropertyNode("status").addConstraintViolation();
return false;
}
return true;
}
}
When I validate Foo
object (by using @Valid
or @Validated
or manually calling the validator.validate()
method), I get following data in the ConstraintViolation
.
Code:
// Update.class is a group
Set<ConstraintViolation<Foo>> constraintViolations = validator.validate(foo, Update.class);
constraintViolations.forEach(constraintViolation -> {
ErrorMessage errorMessage = new ErrorMessage();
errorMessage.setKey(constraintViolation.getPropertyPath().toString());
errorMessage.setValue(constraintViolation.getInvalidValue());
// Do something with errorMessage here
});
constraintViolation.getPropertyPath().toString()
=> status
constraintViolation.getInvalidValue()
=> (Foo object)
How can I set an invalid value (actual value of status
attribute) in custom ConstraintValidator
or anywhere else so that constraintViolation.getInvalidValue()
returns value of status
attribute?
OR
Is there a better way of validating request payload/bean where validation of an attribute depends on another attribute's value?
Edit : I can do something like
if(constraintViolation.getPropertyPath().toString().equals("status")) {
errorMessage.setValue(foo.getStatus());
}
but this would involve maintaining the String constant of attribute names somewhere for eg. "status"
. Though, in the StatusValidator
also, I am setting the attribute name .addPropertyNode("status")
which also I would like to avoid.
Summary : I am looking for a solution (not necessarily using custom validations or hibernate validator) where
You can use dynamic payload to provide additional data in the constraint violation. It can be set using HibernateConstraintValidatorContext
:
context.unwrap(HibernateConstraintValidatorContext.class)
.withDynamicPayload(foo.getStatus().toString());
And javax.validation.ConstraintViolation
can, in turn, be unwrapped to HibernateConstraintViolation
in order to retrieve the dynamic payload:
constraintViolation.unwrap(HibernateConstraintViolation.class)
.getDynamicPayload(String.class);
In the example above, we pass a simple string, but you can pass an object containing all the properties you need.
Note that this will only work with Hibernate Validator, which is the most widely used implementation of the Bean Validation specification (JSR-303/JSR-349), and used by Spring as its default validation provider.
You can use the expression language to evaluate the property path. E.g.
Set<ConstraintViolation<Foo>> constraintViolations = validator.validate(foo);
constraintViolations.forEach(constraintViolation -> {
Path propertyPath = constraintViolation.getPropertyPath();
Foo rootBean = constraintViolation.getRootBean();
Object invalidPropertyValue = getPropertyValue(rootBean, propertyPath);
System.out.println(MessageFormat.format("{0} = {1}", propertyPath, invalidPropertyValue));
});
private static Object getPropertyValue(Object bean, Path propertyPath) {
ELProcessor el = new ELProcessor();
el.defineBean("bean", bean);
String propertyExpression = MessageFormat.format("bean.{0}", propertyPath);
Object propertyValue = el.eval(propertyExpression);
return propertyValue;
}
The expression language does also work with nested beans. Here is a full example
You will need Java >1.8 and the follwing dependencies:
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.0.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.2.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator-annotation-processor</artifactId>
<version>6.0.2.Final</version>
</dependency>
and my java code
public class Main {
public static void main(String[] args) {
ValidatorFactory buildDefaultValidatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = buildDefaultValidatorFactory.getValidator();
// I added Bar to show how nested bean property validation works
Bar bar = new Bar();
// Must be 2 - 4 characters
bar.setName("A");
Foo foo = new Foo();
foo.setBar(bar);
foo.setCompletionDate(LocalDate.now());
// must be complete or closed
foo.setStatus("test");
Set<ConstraintViolation<Foo>> constraintViolations = validator.validate(foo);
System.out.println("Invalid Properties:");
constraintViolations.forEach(constraintViolation -> {
Path propertyPath = constraintViolation.getPropertyPath();
Foo rootBean = constraintViolation.getRootBean();
Object invalidPropertyValue = getPropertyValue(rootBean, propertyPath);
System.out.println(MessageFormat.format("{0} = {1}", propertyPath, invalidPropertyValue));
});
}
private static Object getPropertyValue(Object bean, Path propertyPath) {
ELProcessor el = new ELProcessor();
el.defineBean("bean", bean);
String propertyExpression = MessageFormat.format("bean.{0}", propertyPath);
Object propertyValue = el.eval(propertyExpression);
return propertyValue;
}
@StatusValidate
public static class Foo {
private String status;
private LocalDate completionDate;
@Valid
private Bar bar;
public void setBar(Bar bar) {
this.bar = bar;
}
public Bar getBar() {
return bar;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public LocalDate getCompletionDate() {
return completionDate;
}
public void setCompletionDate(LocalDate completionDate) {
this.completionDate = completionDate;
}
}
public static class Bar {
@Size(min = 2, max = 4)
private String status;
public String getStatus() {
return status;
}
public void setName(String status) {
this.status = status;
}
}
@Constraint(validatedBy = StatusValidator.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public static @interface StatusValidate {
String message()
default "default status error";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public static class StatusValidator implements ConstraintValidator<StatusValidate, Foo> {
@Override
public boolean isValid(Foo value, ConstraintValidatorContext context) {
if (null != value.getCompletionDate() && (!value.getStatus().equalsIgnoreCase("complete")
&& !value.getStatus().equalsIgnoreCase("closed"))) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
.addPropertyNode("status").addConstraintViolation();
return false;
}
return true;
}
}
}
Output is:
Invalid Properties:
status = test
bar.status = A
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With