I am trying to validate my rest request according to some fields existance. For example, if transactionDate field is null or didnt exist in my request object, I want to throw an error to client.
I couldn't do it despite the source of this guide and still my requests can pass in controller.
How can I validate two or more fields in combination?
DTO
#FraudRestRequestValidator
public class FraudActionsRestRequest {
private BigDecimal amount;
private String receiverTransactionDate;
private String receiverNameSurname;
private BigDecimal exchangeRate;
private String transactionReferenceNumber;
#NotNull
private String transactionDate;
#NotNull
private String transactionTime;
private String transactionTimeMilliseconds;
private BigDecimal tlAmount;
private String channel;
}
ANNOTATION
#Constraint(validatedBy = FraudActionsRestValidator.class)
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
public #interface FraudRestRequestValidator {
String message() default "Invalid Limit of Code";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
VALIDATOR
public class FraudActionsRestValidator implements ConstraintValidator<FraudRestRequestValidator, FraudActionsRestRequest> {
#Override
public void initialize(FraudRestRequestValidator constraintAnnotation) {
}
#Override
public boolean isValid(FraudActionsRestRequest fraudActionsRestRequest, ConstraintValidatorContext constraintValidatorContext) {
//I will implement my logic in future
return false;
}
}
REST CONTROLLER
#PostMapping("/getFraudActions")
public ResponseEntity<?> getFraudActions(#Valid #RequestBody FraudActionsRestRequest fraudActionsRestRequest, Errors errors) throws Exception
Thanks.
In your custom validator just implement logic you want to have. You did everything correct except some minor thing:
#Constraint(validatedBy = FraudActionsRestValidator.class)
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
public #interface ValidFraudRestRequest {
String message() default "Invalid Limit of Code";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class FraudActionsRestValidator implements ConstraintValidator<ValidFraudRestRequest, FraudActionsRestRequest> {
#Override
public void initialize(ValidFraudRestRequest constraintAnnotation) {
}
#Override
public boolean isValid(FraudActionsRestRequest fraudActionsRestRequest, ConstraintValidatorContext constraintValidatorContext) {
return fraudActionsRestRequest.getTransactionDate() != null && fraudActionsRestRequest.getTransactionTime() != null && additional check you need;
}
}
Looks all okaish.
You might be missing the #Validated annotation on the rest controller class,
See https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-validation.html for more info
Related
Say I have a request payload class like the following:
#Data
class Payload {
Type type;
#Valid
Details details;
}
And,
#Data
class Dtails {
#OnlyAlphabet
String name;
String email;
}
I have the enum defined as follows:
public enum Type {
HUMAN,
ALIEN
}
I have defined the #OnlyAlphabet constraint like this:
#Target({TYPE, FIELD, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Constraint(validatedBy = NameValidator.class)
public #interface OnlyAlphabet {
String message() default "Invalid Name";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
And,
public class NameValidator implements ConstraintValidator<OnlyAlphabet, String> {
#Override
public void initialize(OnlyAlphabet constraintAnnotation) {
}
#Override
public boolean isValid(String name, ConstraintValidatorContext constraintValidatorContext) {
return isContainOnlyAlphabet(name);
}
}
If the type is ALIEN, I want the #OnlyAlphabet validation constraint disabled. Or even, based on the type property, I want to use another annotation. Is there any way I can achieve it?
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.
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
Use #NotNull for Completion date and use Custom enum validator for status like this :
/*enum class*/
public enum Status{
COMPLETE,
CLOSED
}
/*custom validator*/
#ValueValidator(EnumValidatorClass = Status.class)
#NotNull
private String status;
#NotNull
private LocalDate completionDate;
/*anotation interface*/
#Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = EnumValueValidator.class)
#Documented
public #interface ValueValidator {
public abstract String message() default "Invalid Status!";
public abstract Class<?>[] groups() default {};
public abstract Class<? extends Payload>[] payload() default {};
public abstract Class<? extends java.lang.Enum<?>> EnumValidatorClass();
}
/*anotation implementation*/
public class EnumValueValidator implements ConstraintValidator<ValueValidator, String>{
private List<String> values;
#Override
public void initialize(ValueValidator annotation)
{
values = Stream.of(annotation.EnumValidatorClass().getEnumConstants()).map(Enum::name).collect(Collectors.toList());
}
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return true;
}
return values.contains(value);
}}
I've met such a misundestrood:
I've made a validation using my own annotation #Category and class CategoryValidator implements ConstraintValidator<Category, String>
All is about list.add() which causes (without it works properly):
Request processing failed; nested exception is
javax.validation.ValidationException: HV000028: Unexpected exception
during isValid call.
Can someone give me any explaination?
CategoryValidator
#Component
public class CategoryValidator implements ConstraintValidator<Category, String> {
#Autowired
ProductService productService;
List<String> allowedCategories;
#Override
public void initialize(Category constraintAnnotation) {
allowedCategories = Arrays.asList(constraintAnnotation.allowedCategories());
}
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// This .add() it not working here
// allowedCategories.add("Laptop");
for (String category : allowedCategories) {
if (category.equalsIgnoreCase(value)) {
return true;
}
}
return false;
}
}
Category
#Target( { METHOD, FIELD, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Constraint(validatedBy = CategoryValidator.class)
#Documented
public #interface Category {
String message() default "{com.ehr.webstore.validator.Category.message}";
Class<?>[] groups() default {};
public abstract Class<? extends Payload>[] payload() default {};
String[] allowedCategories();
}
Product
public class Product {
/* [...] */
#Category(allowedCategories = {"Laptop", "Tablet", "Smart Phone", "Another"})
private String category;
/* [...] */
}
Well, it would be easier with the full stacktrace but I think your issue is that you can't add an element to a List created by Arrays.asList(): it is immutable.
If you want to be able to add new elements, you need to do something like: new ArrayList<>( Arrays.asList( ... ) ).
I am trying to extend the behavior of the #NotBlank constraint to apply to URIs by making a custom constraint called #NotBlankUri.
Here's my constraint annotation:
#Target({ METHOD, FIELD })
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = NotBlankUriValidator.class)
public #interface NotBlankUri {
String message() default "{project.model.NotBlankUri.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
and here is the ConstraintValidator:
public class NotBlankUriValidator implements ConstraintValidator<NotBlankUri, URI> {
public void initialize(NotBlankUri annotation) {
}
public boolean isValid(URI uri, ConstraintValidatorContext context) {
NotBlankValidator nbv = new NotBlankValidator();
return nbv.isValid(uri.toString(), context);
}
}
Problem is that the isValid() method on the ConstraintValidator is getting null values for the URI argument. I thought this wasn't supposed to happen given the fact that #NotBlank itself is annotated #NotNull. That not being the case, I tried adding #NotNull as a meta-annotation to my #NotBlankUri, but that didn't have the desired effect either. How can I make my annotation constraint behave like #NotBlank, which seems to be stacking on top of the behavior of #NotNull?
As per the documentation, you can't use the #NotBlank annotation on a datatype that is not a String.
public #interface NotBlank
Validate that the annotated string is not null or empty. The difference to NotEmpty is that trailing whitespaces are getting ignored.
So if you declared your validator to validate a String, everything would be fine and you could write your annotation like this:
#Target({ METHOD, FIELD })
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = NotBlankUriValidator.class)
#NotBlank
public #interface NotBlankUri {
String message() default "{project.model.NotBlankUri.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
If you are deadset on using the URI class 1 you need to perform custom validation logic yourself like this:
Annotation:
#NotNull(message="URI must not be null")
#Target({ METHOD, FIELD })
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = NotBlankUriValidator.class)
public #interface NotBlankUri {
String message() default "URI must not be blank";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Validator:
public class NotBlankUriValidator implements ConstraintValidator<NotBlankUri, URI> {
public void initialize(NotBlankUri annotation) {
}
public boolean isValid(URI uri, ConstraintValidatorContext context) {
boolean isValid = true;
System.out.println("URI: " + uri);
//Leave null checks to your #NotNull constraint.
//This is only here to prevent a NullPointerException on the next check.
if(uri == null){
return true;
}
if(uri.toString().isEmpty()){
isValid = false;
}
return isValid;
}
}
I ran the above with a test harness:
public class UriContainer {
public UriContainer(URI uri){
this.uri = uri;
}
#NotBlankUri
private URI uri;
public URI getUri() {
return uri;
}
}
public static void main(String[] args) throws URISyntaxException{
UriContainer filledContainer = new UriContainer(new URI("Stuff"));
UriContainer emptyContainer = new UriContainer(new URI(""));
UriContainer nullContainer = new UriContainer(null);
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<UriContainer>> filledViolations = validator
.validate(filledContainer);
Set<ConstraintViolation<UriContainer>> emptyViolations = validator
.validate(emptyContainer);
Set<ConstraintViolation<UriContainer>> nullViolations = validator
.validate(nullContainer);
System.out.println("Filled: ");
filledViolations.stream().forEach(System.out::println);
System.out.println("Empty: ");
emptyViolations.stream().forEach(System.out::println);
System.out.println("Null: ");
nullViolations.stream().forEach(System.out::println);
}
which output the following violations:
URI: Stuff
URI:
URI: null
Filled:
Empty:
ConstraintViolationImpl{interpolatedMessage='URI must not be blank', propertyPath=uri, rootBeanClass=class sandbox.UriContainer, messageTemplate='URI must not be blank'}
Null:
ConstraintViolationImpl{interpolatedMessage='URI must not be null', propertyPath=uri, rootBeanClass=class sandbox.UriContainer, messageTemplate='URI must not be null'}
As you can see, this allows you to output different error messages based on if the URI is blank or null. Just make sure if you are using a javax.validation annotation you check which datatype you operate on.
1: which by the way, performs validation when you construct the object, and will throw a URISyntaxException if the String passed to the constructor violates RFC 2396
I am trying to use spring to check user online input to ensure that the two characters they enter is an actual US state, is there any way of doing this, hopefully using a preset pattern? like, #State or something (if that was a legit annotation). Also, is there a good annotation commonly used for a String street, and String city field? That is other than #NotNull and #NotEmpty
Any help would be greatly appreciated!!
Unfortunately there is no out of the box however you can create your own #State annotation , all you need is to define your annotation and class implementing ConstraintValidator(which handles the validation logic) E.g.
#Constraint(validatedBy = StateConstraintValidator.class)
#Target( { ElementType.FIELD })
#Retention(RetentionPolicy.RUNTIME)
public #interface State {
String message() default "{State}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class StateConstraintValidator implements ConstraintValidator<String, String> {
private static final Set<String> CODE_MAP = new HashSet<>(){
{add("AR");}
{add("AK");} //add more codes ...
};
#Override
public void initialize(String state) { }
#Override
public boolean isValid(String value, ConstraintValidatorContext cxt) {
if(value == null) {
return false;
}
return CODE_MAP.contains(value);
}
}
In the similar manner you can create other annotations.