Java Spring Boot custom validation: generic validation for all enums - java

For a SpringBoot project I'm using custom validation using custom annotations.
In particular, I have a lot of enums and I would like to validate if into the inputs there are correct enums values (e.g. from json to DTO objects)
I'm using this approach:
I defined my annotation:
#Target( { FIELD, PARAMETER })
#Retention(RetentionPolicy.RUNTIME)
#Documented
#Constraint(validatedBy = PlatformValidator.class)
public #interface PlatformValidation {
String message() default "Invalid platform format";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
and my validator:
public class PlatformValidator implements ConstraintValidator<PlatformValidation, String>
{
public boolean isValid(String platform, ConstraintValidatorContext cxt) {
try {
if (!isNull(platform)) {
Platform.valueOf(platform);
}
return true;
} catch (Exception ex) {
return false;
}
}
}
My question is, can I have a generic enum validation annotation and specify the enum that I need as a parameter of my annotation?
Something like #MyValidation(enum=MyEnum, message= ...)

For the generics approach is use Java reflection. So the annotation should be:
#EnumValidation(enum=YourEnum.class, message=...)
The tutorial about reflection: Jenkov's Tutorial

Related

How to validate enum in DTO?

In the domain model object I have the following field:
private TermStatus termStatus;
TermStatus is an enum:
public enum TermStatus {
NONE,
SUCCESS,
FAIL
}
In the DTO, I have the same field as in the domain object. The question is, how can I validate the passed value? If the API client now passes an incorrect string with the enum value as a parameter (for example, nOnE), it will not receive any information about the error, only the status 400 Bad Request. Is it possible to validate it like this, for example, in the case of javax.validation annotations like #NotBlank, #Size, where in case of an error it will at least be clear what it is. There was an idea to make a separate mapping for this, for example "items/1/complete-term" instead of direct enum transmission, so that in this case the server itself would set the SUCCESS value to the termStatus field. But as far as I know, these things don't look very good in REST API, so I need your ideas
Instead of validating enum directly, you could check whether String is valid for specific enum. To achieve such an effect you could create your own enum validation annotation.
#Documented
#Constraint(validatedBy = EnumValidatorConstraint.class)
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.FIELD)
#NotNull
public #interface EnumValidator {
Class<? extends Enum<?>> enum();
String message() default "must be any of enum {enum}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Then you need to implement validator to check whether String exist as a part of this enum.
public class EnumValidatorConstraint implements ConstraintValidator<EnumValidator, String> {
Set<String> values;
#Override
public void initialize(EnumValidator constraintAnnotation) {
values = Stream.of(constraintAnnotation.enumClass().getEnumConstants())
.map(Enum::name)
.collect(Collectors.toSet());
}
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return values.contains(value);
}
}
Lastly, you need to annotate your field with #EnumValidator.
#EnumValidator(enum = TermStatus.class)
private String termStatus;
In case of not matching String a MethodArgumentNotValidException will be thrown. Same as for #NotNull or other constraint validation.
Sounds like you need to implement your own response after validation and tell the API client that the data in your received DTO is invalid and return message with the actual received value (nOnE in your case) and maybe the list of your valid values (if that's not gonna be a security concern). Also, I think the ideal http status for your response would be 422 instead of a generic 400 Bad Request.
For your actual validation implementation, I think you can just directly compare the converted value from DTO to ENUM of the data you received from the API client against your ENUM values in the back-end. If equals to any of the ENUM values, then it's a valid request (200) else, 422.
Hope this helps!
You can make a utility method inside your enum like below
private String text;
TermStatus(String text) {
this.text = text;
}
public static TermStatus fromText(String text) {
return Arrays.stream(values())
.filter(bl -> bl.text.equalsIgnoreCase(text))
.findFirst()
.orElse(null);
}
And set value in dto like below
dto.setTermStatus(TermStatus.fromText(passedValue))
if(dto.getTermStatus()== null)
throw new Exception("Your message");
Hope this helps!
You should use String data type for termStatus. Because of client sends String value for this. Then you have to create Custom validation constraints to fix this as below.
ValueOfEnum
#Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
#Retention(RUNTIME)
#Documented
#Constraint(validatedBy = ValueOfEnumValidator.class)
public #interface ValueOfEnum
{
Class<? extends Enum<?>> enumClass();
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
ValueOfEnumValidator
public class ValueOfEnumValidator implements ConstraintValidator<ValueOfEnum, CharSequence>
{
private List<String> acceptedValues;
#Override
public void initialize(ValueOfEnum annotation)
{
acceptedValues = Stream.of(annotation.enumClass().getEnumConstants())
.map(Enum::name)
.collect(Collectors.toList());
}
#Override
public boolean isValid(CharSequence value, ConstraintValidatorContext context)
{
if (value == null) {
return true;
}
return acceptedValues.contains(value.toString());
}
}
Now you can #ValueOfEnum annotation for your domain model. Then add #Validated annotation in front of your controller class domain object.
#ValueOfEnum(enumClass = TermStatus.class, message = "Invalid Term Status")
private String termStatus;

Is it possible to pass validatedBy attribute to the annotation as a parameter?

I hard coded the validatedBy value as following.
#Constraint(validatedBy = ReminderValidator.class)
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
public #interface MyValidator{
String message() default "{error.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Instead of hard coding the validatedBy value into my annotation interface, I want to pass it as a parameter something similar to the following.
#MyValidator(validatedBy = "ReminderValidator.class")
public class Reminder {
...
}
This will enable me to create just 1 annotation for all class validations. I will just provide different validator classes to the annotation to validate different classes. Is it possible to do something like this?
You can add multiple validators in the #Constraint annotation and it will pick the appropriate based on the object type.
#Constraint(validatedBy = { ReminderValidator.class, PendingValidator.class } )
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
public #interface MyValidator{
String message() default "{error.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
And
#MyValidator()
public class Reminder {
...
}
#MyValidator()
public class Pending {
...
}
Validator Examples
public class ReminderValidator implements ConstraintValidator<MyValidator, Reminder> {
...
public class PendingValidator implements ConstraintValidator<MyValidator, Pending> {
...
Let me know if this is an acceptable solution for you -
#Constraint(validatedBy = CommonValidator.class)
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
public #interface MyValidator{
// this decides what actual validator we will use
String validator();
String message() default "{error.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Now in the CommonValidator we can decide the actual validator based on the value of validator.
Example of a static validator:
public class SomeValidator {
public static boolean isValid(Object field, ConstraintValidatorContext context) {
// your actual validation code
}
}
Similarly you can define your other validators as well.
This is how you can use the static validator in actual validator :
public class CommonValidator implements ConstraintValidator<MyValidator, Object> {
private String validator;
#Override
public void initialize(MyValidator myValidator) {
this.validator = myValidator.validator();
// do other inits as per your requirement
}
#Override
public boolean isValid(Object field, ConstraintValidatorContext context) {
ValidatorFactory.getValidator("someValidator").isValid(field, context)
// return other validators like above
return false;
}
}
finally you can use you annotation like this -
#MyValidator(validator = "someValidator")
private Object object;
Object can be any other class as well, you have to handle this in
individual static validator.
FYI - this is just an idea, Actual implementation might differ as per your use case.

How to enable javax annotations when value not null and disabled when value is null?

I need to disable validation if the value of email is null and check on it if the value is not null.
#Email(message = "{invalidMail}")
private String email;
I found the answer and it's that almost all java validation annotations accepts null so if my value is null it's going to accept it otherwise it will check.
You can't achieve this result with the predefined set of validation annotations only.
You have to create a custom validation annotation which performs the validation based on the specifications. You can get inspired on the Baeldung's article Spring MVC Custom Validation.
Here is the annotation.
#Documented
#Constraint(validatedBy = MyEmailValidator.class) // Class which performsthe validation
#Target({ ElementType.FIELD }) // Applicable to a field
#Retention(RetentionPolicy.RUNTIME)
public #interface MyEmail {
String message() default "The email is invalid"; // The default message
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
And a class which actually performs the validation itself against the email Regex if the input is not null. Returns true otherwise as null is accepted. Note this class has to implement ConstraintValidator<A extends Annotation, T>.
public class MyEmailValidatorimplements ConstraintValidator<MyEmail, String> {
// Email Regex
private final String emailPattern= "[A-Z0-9._%+-]+#[A-Z0-9.-]+\\.[A-Z]{2,6}";
#Override
public void initialize(MyEmail myEmail) { }
#Override
public boolean isValid(String input, ConstraintValidatorContext cxt) {
if (input == null) {
return true;
} else return Pattern.matches(emailPattern, input);
}
}

Spring validation annotation - How can I verify that a 2 character entry into a string is an actual US state?

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.

Dropwizard Validation Error Message

Is there a way to add variables in Dropwizard's validation error message? Something in the effect of
#ValidationMethod(message=String.format("Url cannot be null, field value = %s", fieldValue))
public boolean isNotValid() {
String fieldValue = this.getFieldValue();
return this.url == null;
}
I just want to add variable into the error message.
I found an answer. Hibernate 5.1 has error message interpolation which kinda takes care of this.
#Size(min = 0, max = 0, message="${validatedValue} is present"))
public String getErrorMessage() {
List<String> illegalValues = ImmutableList.of("illegal value");
return illegalValues;
}
It's a little hacky, but it solves the problem. Take a look at http://docs.jboss.org/hibernate/validator/5.1/reference/en-US/html/chapter-message-interpolation.html
I found a proper way to validate beans - don't use dropwizard's #ValidationMethod, but instead define your own annotation and validator:
#Target({TYPE, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Constraint(validatedBy = MyValidator.class})
#Documented
public #interface ValidPerson {
String message() default "Bad person ${validatedValue.name}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
class MyValidator implements ConstraintValidator<ValidPerson,Person> {
#Override
public void initialize(ValidPerson annotaion) {}
#Override
public boolean isValid(Person p, ConstraintValidatorContext context) {
return false; //let's say every person is invalid :-)
}
}
I'm unfamiliar with Dropwizard, but Java's annotations are simply compile-time metadata. You can't call methods in annotation declarations simply because the Java compiler does not perform the same compile-time code execution as some other compilers, such as C or C++.

Categories

Resources