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
- I can validate json requestor or a bean, for an attribute whose validations depends on values of other attributes.
- I don't have to maintain bean attribute names as String constants anywhere (maintenance nightmare).
- I am able to get the invalid property name and value both.
constraintViolation.getInvalidValue()
isinstantOf
Foo
, and cast it and get the value of status? – RunaboutconstraintViolation.getPropertyPath()
and then get the value fromFoo
object based on it. I was trying to find a way to avoid this manual process. – Politicocontext.unwrap(HibernateConstraintValidatorContext.class).addExpressionVariable("status", value.getStatus())
and refer to it in the validation message using${status}
– Renault@FieldNameConstants
– Renault