I use javax.validation.constraints annotations to validate the request parameters. I need to validate emails so I've created an annotation #EmailValid, snippet provided below:
#Email(message = "Please provide a valid email address")
#Pattern(regexp = ".+#.+\\..+", message = "Please provide a valid email address")
#Target({METHOD, FIELD, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Constraint(validatedBy = {})
#Documented
public #interface EmailValid {
String message() default "Please provide a valid email address";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
But it works only for single String field e.g. :
#ResponseStatus(value = HttpStatus.OK)
#RequestMapping(value = "/sendEmail", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ResponseVO> sendEmail(#EmailValid String email) {
// some code for sending email
}
What I need is to validate Map of emails. I have a map Map<String, String> users where key is userId and value is an email. So I have a method like this in my controller:
#ResponseStatus(value = HttpStatus.OK)
#RequestMapping(value = "/sendEmails", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ResponseVO> sendEmails(Map<String, String> users) {
// some code for sending email
}
Question is how can I validate map values here using my annotation #EmailValid and other javax.validation.constraints annotations?
You have to implement custom validation like below.
First Change your EmailValid.java file.
#Target({METHOD, FIELD, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Constraint(validatedBy = { EmailValidCheck.class })
#Documented
public #interface EmailValid {
String message() default "Please provide a valid email address";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Now Implement This ConstraintValidator like Below.
public class EmailValidCheck extends ABaseController implements
ConstraintValidator<EmailValid, Map<String,String>> {
#Override
public void initialize(EmailValid constraintAnnotation) {
}
#Override
public boolean isValid(Map<String,String> mapOfEmail,
ConstraintValidatorContext cvc) {
//Implement Email Validation Login Here.
}
}
Change Your Controller Like Below.
#ResponseStatus(value = HttpStatus.OK)
#RequestMapping(value = "/sendEmails", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ResponseVO> sendEmails(#EmailValid Map<String, String> users) {
// some code for sending email
}
Related
I have a rest service with my request body bean annotated with javax.validation like #NotBlank #NotNull #Pattern etc., and in one specific field I receive a file encoded as a string base64,
so, is there an annotation, or how could I write a custom validation annotation, so it would check if the string is really a base64 string?
I just need a validation like this in annotation form:
try {
Base64.getDecoder().decode(someString);
return true;
} catch(IllegalArgumentException iae) {
return false;
}
thnx in advance
Yes, you could write your own annotations and validators for them.
Your annotation would look like this:
#Documented
#Constraint(validatedBy = Base64Validator.class)
#Target( { ElementType.FIELD })
#Retention(RetentionPolicy.RUNTIME)
public #interface IsBase64 {
String message() default "The string is not base64 string";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Constraint validator javax.validation implementation (I'm using here your code for the actual validation):
public class Base64Validator implements ConstraintValidator<IsBase64, String> {
#Override
public boolean isValid(String value, ConstraintValidatorContext cxt) {
try {
Base64.getDecoder().decode(value);
return true;
} catch(IllegalArgumentException iae) {
return false;
}
}
}
Example data class with the annotated field:
#Data
public class MyPayload {
#IsBase64
private String value;
}
And controller method example with #Valid annotation which is required:
#PostMapping
public String test(#Valid #RequestBody MyPayload myPayload) {
return "ok";
}
I have created a custom validator that should validate if the body of a request (a simple string) is in Json format. I see that the custom validator is never called. Here are some parts of my code:
#RequestMapping(value = "/endpoint", method = { RequestMethod.POST })
#ResponseBody
public ResponseEntity<String> authorize(#RequestBody #MyValidator String token) {
// logic
}
This is the annotation:
#Target({ ElementType.PARAMETER })
#Retention(RUNTIME)
#Constraint(validatedBy = { JsonSyntaxValidator.class })
#Documented
public #interface MyValidator {
String message() default "{Token is not in Json syntax}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
This is the validator:
public class JsonSyntaxValidator implements ConstraintValidator<MyValidator, String> {
#Override
public void initialize(JsonFormat constraintAnnotation) {
}
/**
* It returns true if the Google Pay or Apple Pay token is in Json format.
*/
#Override
public boolean isValid(String token, ConstraintValidatorContext constraintValidatorContext) {
boolean isValid = true;
try {
JsonParser.parseString(token);
} catch (JsonSyntaxException e) {
isValid = false;
}
return isValid;
}
}
I have tried invoking the endpoint with postman passing it a string not formatted as json and in debug I see that the check is skipped past.
I don't want to have a POJO with fields, I just want the request body as a string.
I haven't found much online, only a post stating that it might not be possible.
Any help would be really appreciated :)
Your controller should be using the #Valid annotation instead of #MyValidator. I updated your controller below to what should work.
public ResponseEntity<String> authorize(#Valid #RequestBody String token) {
// logic
}
I have following annotation to validate password:
#Target({FIELD})
#Retention(RUNTIME)
#Documented
#NotNull
#Length(min = 8, max = 32)
#Pattern(regexp = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[##$%^&+=])(?=\\S+$).{8,}$")
public #interface Password {
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
But spring validation does not recognize this rules. I used this annotation as:
#Password
private String password;
How can I get it without defining ConstraintValidator instance?
If you want to use ConstraintValidator, you can do it like this:
create Password annotation :
#Documented
#Constraint(validatedBy = PasswordConstraintValidator.class)
#Target({ FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
#Retention(RUNTIME)
public #interface Password {
String message() default "{propertyPath} is not a valid password";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
then create the PasswordConstraintValidator class :
public class PasswordConstraintValidator implements ConstraintValidator<Password, String> {
private final String PASSWORD_PATTERN =
"^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!##&()–[{}]:;',?/*~$^+=<>]).{8,20}$";
private final Pattern pattern = Pattern.compile(PASSWORD_PATTERN);
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if(Objects.isNull(value)) {
return false;
}
if((value.length() < 8) || (value.length() > 32)) {
return false;
}
if(!pattern.matcher(password).matches()){
return false;
}
}
Then apply it to one of your fields, note that you can also put a custom message:
#Password(message = "....")
private String password;
#Password
private String passwd;
You can also refactor the if statements each in an appropriate method (to have a clean code): something that will look like this :
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return (notNull(value) && isValidPasswordLength(value) && isValidPasswordValue(value));
}
Update
since you don't want to use the ConstraintValidator, your implementation looks fine, you just need to add #Valid on your model so that cascading validation can be performed and include spring-boot-starter-validation to make sure that validation api is included and add #Constraint(validatedBy = {}) on your custom annotation. Here is a groovy example here (you can run it with spring CLI) :
#Grab('spring-boot-starter-validation')
#Grab('lombok')
import lombok.*
#Grab('javax.validation:validation-api:2.0.1.Final')
import javax.validation.constraints.NotNull
import javax.validation.constraints.Size
import javax.validation.Valid
import javax.validation.Constraint
import javax.validation.Payload
import java.lang.annotation.Documented
import java.lang.annotation.Target
import java.lang.annotation.Retention
import static java.lang.annotation.ElementType.FIELD
import static java.lang.annotation.RetentionPolicy.RUNTIME
#RestController
class TestCompositeAnnotation {
#PostMapping(value = "/register", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public String register(#Valid #RequestBody User user) {
return "password " + user.password + " is valid";
}
}
class User {
public String username;
#Password
public String password;
}
#Target(value = FIELD)
#Retention(RUNTIME)
#Documented
#NotNull
#Constraint(validatedBy = []) // [] is for groovy make sure to replace is with {}
#Size(min = 8, max = 32)
#interface Password {
String message() default "invalid password";
Class<?>[] groups() default []; // [] is for groovy make sure to replace is with {}
Class<? extends Payload>[] payload() default []; // [] is for groovy make sure to replace is with {}
}
So when you curl :
curl -X POST http://localhost:8080/register -d '{"username": "rsone", "password": "pa3"}' -H "Content-Type: application/json"
you will get an error validation response :
{"timestamp":"2020-11-07T16:43:51.926+00:00","status":400,"error":"Bad Request","message":"...","path":"/register"}
I write simple validation annotation for spring project. Problem is that I don't understand some method purpose.
Here is my annotation:
#Constraint(validatedBy = PostCodeValidator.class)
#Target({ElementType.METHOD, ElementType.FIELD})
#Retention(RetentionPolicy.RUNTIME)
public #interface PostCode {
public String value() default "LUV";
public String message() default "must start with LUV";
public Class<?>[] groups() default {};
public Class<? extends Payload>[] payload() default {};
}
Could anyone explain me groups() & payload() method purpose? I'll be very grateful if explanation will be as simple as it possible. Thanks.
1) In Bean Validation Api, groups are used for selecting which fields of bean will be validated. Example : a User with Address property.
public class User {
#NotNull(groups = GroupUser.class)
String firstname;
#NotNull(groups = GroupAddress.class)
String street;
#NotNull(groups = GroupAddress.class)
String city;
}
To validate the whole user you can do :
Set<ConstraintViolation<User>> constraintViolations = validator.validate(user, GroupUser.class, GroupAddress.class);
To validate only the user information without address part you can use :
Set<ConstraintViolation<User>> constraintViolations = validator.validate(user, GroupUserName.class);
2) Payload is used to manage the severity level of error. You can implement Payload interface :
public class Severity {
public static class Info implements Payload {}
public static class Error implements Payload {}
}
If you annotate the firstname field with :
#NotNull(payload = {Severity.Error.class})
Then after validation you can add some logic by severity :
for (ConstraintViolation<TestBean> violation : constraintViolations) {
Set<Class<? extends Payload>> payloads = violation.getConstraintDescriptor().getPayload();
for (Class<? extends Payload> payload : payloads) {
if (payload == Severity.Error.class) {
// logic here
}
}
}
There is a custom validation annotation created to check if two spring form fields are equal or not.
PasswordVerification:
#Constraint(validatedBy = PasswordVerificationValidator.class)
#Target({ElementType.TYPE, ElementType.FIELD})
#Retention(RetentionPolicy.RUNTIME)
public #interface PasswordVerification {
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
PasswordVerificationValidator:
public class PasswordVerificationValidator implements ConstraintValidator<PasswordVerification, UserFormRegistration> {
#Override
public void initialize(PasswordVerification constraintAnnotation) {}
#Override
public boolean isValid(UserFormRegistration userFormRegistration, ConstraintValidatorContext context) {
return userFormRegistration.getPassword().equals(userFormRegistration.getVerifyPassword());
}
}
UserFormRegistration:
#PasswordVerification(message = "Password and password confirmation fields don't match")
public class UserFormRegistration {
private String password;
...
So, if the annotation is applied to the class UserFormRegistration, it works fine. But if I want to apply it to the field (see below), it fails.
public class UserFormRegistration {
#PasswordVerification(message = "Password and password confirmation fields don't match")
private String password;
...
Exception:
javax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint 'ua.com.vertex.validators.interfaces.PasswordVerification' validating type 'java.lang.String'. Check configuration for 'password'
How to fix?
I guess you want to apply the annotation at method level also so you need to have ElementType.METHOD
so change #Target({ElementType.TYPE, ElementType.FIELD}) to
#Target({ElementType.TYPE, ElementType.FIELD,ElementType.METHOD})
so now #PasswordVerification will be applicable to methods, classes,interfaces,enums and fields