How to get which field is failed in custom jakarta bean validation? - java

I added custom validation as annotation and add it to my DTO's as #UUID and it works expected. I add this annotation whenever I need to validate if a field is valid UUID.
Whenever it fails it throws exception with GlobalExceptionHandler as I expect.
{
"exception": "org.springframework.web.bind.MethodArgumentNotValidException",
"timeStamp": 1676468004793,
"errorDetail": [
{
"key": "invalid.uuid",
"message": "Invalid UUID",
"errorCode": null,
"args": null
}
]
}
How can I make validator to tell me which field has failed when I have such dto ?
DTO
#Data
public class CollectionCreateRequest {
#UUID
private String gitlabId = null;
#UUID
private String teamId;
}
Validator
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import jakarta.validation.ReportAsSingleViolation;
import jakarta.validation.constraints.Pattern;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
#Target(ElementType.FIELD)
#Constraint(validatedBy={})
#Retention(RetentionPolicy.RUNTIME)
#Pattern(regexp="^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$|")
#ReportAsSingleViolation
public #interface UUID {
String message() default "invalid.uuid";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

What about:
#UUID(message="gitlabId: Invalid UUID")
private String gitlabId = null;
#UUID(message="teamId: Invalid UUID")
private String teamId;

As you are using Spring and GlobalExceptionHandler you would want to make sure that you have a handler for MethodArgumentNotValidException in it. Somethig along next lines would do it:
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ExceptionHandler(MethodArgumentNotValidException.class)
public Problem handleValidationExceptions(MethodArgumentNotValidException ex) {
ex.getFieldErrors().forEach((error) -> {
FieldError fieldError = (FieldError) error;
String fieldName = fieldError.getField();
String errorMessage = error.getDefaultMessage();
// get any other properties you need
// create a response based on the collected info
});
// return your built error result
}

Related

javax.validation annotation for string base64

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";
}

List not honoring #NotEmpty annotation

I have a list that is filled with a request body. I expect 400 BAD Request response status when No value or Null is passed in request.
is working as expected when No value is being passed. But for Null, it does not throw 400. How can I make it work?
class data{
#NotEmpty
private List<#Valid String> values;
}
Request body 1 -> getting response status 200. This is expected.
{
"values": [
"randomValue"
]
}
Request body 2 -> getting response status 400 (VALIDATION_ERROR) . This is expected.
{
}
Request body 3 -> getting response status 400 (VALIDATION_ERROR) . This is expected.
{
"values": [
]
}
Request body 4 -> getting response status 200. Expected status 400 (VALIDATION_ERROR).
{
"values": [
null
]
}
This is because an array/list with null elements is not empty. You can handle this by defining a new custom validation for validating the list input. See an example below:
Define a new Annotation ValidList (you can name it something else too as per your liking). Note the validatedBy attribute - this is the class that will do the actual validation for the fields annotated with this annotation.
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
#Documented
#Retention(RetentionPolicy.RUNTIME)
#Target({ElementType.FIELD, ElementType.PARAMETER,
ElementType.ANNOTATION_TYPE})
#Constraint(validatedBy = ListValidator.class)
public #interface ValidList {
String message() default "Array/List field cannot be null";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Define the actual validator class - validation is handled by this custom validator ListValidator (code below):
import java.util.List;
import java.util.Objects;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class ListValidator implements ConstraintValidator<ValidList, List<? extends Object>> {
#Override
public boolean isValid(List<? extends Object> list,
ConstraintValidatorContext context) {
//NOTE: this condition will mark the list invalid even if there is a single null element present in the list (you can change it to fit your use-case)
return !(list == null || list.isEmpty() || list.stream().anyMatch(Objects::isNull));
}
#Override
public void initialize(ValidList constraintAnnotation) {}
}
Now, just use the newly created ValidList annotation on your Data class:
public class Data {
#ValidList
private List<#Valid String> values;
public List<String> getValues() {
return values;
}
public void setValues(List<String> values) {
this.values = values;
}
public Data() {
}
public Data(#NotEmpty List<#Valid String> values) {
this.values = values;
}
}
You can use the #NotNull annotation for list elements:
#NotEmpty
private List<#NotNull String> values;
EDIT:
An example of Controller method:
#GetMapping
public List<String> get(#RequestBody #Valid Data data) {
return data.getValues();
}

How to join several validation annotations

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"}

Combing error message for multiple field validation

I’m checking if two fields contain and invalid characters, if so, output an error message.
This works fine when its just one field that is invalid but when both contain invalid characters, I need a single error message with details of both fields.
Example:
Single invalid field = Fields contain invalid characters: account number '6637=958'
Both field invalid = Fields contain invalid characters: account number '6637=958', sort code '01%657'
I can’t change the format of the error messages. Right now, I just use a stream to output the first error message. I would like to keep the solution simple but can only think of brute force ways to combine the messages.
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
#JsonIgnoreProperties(ignoreUnknown = true)
public class AccountDetails {
#Pattern(regexp = "^[-0-9]*$", message="Fields contain invalid characters: account number '${validatedValue}'")
private String accountNumber;
#Pattern(regexp = "^[-0-9]*$", message="Fields contain invalid characters: sort code '${validatedValue}'")
#NotBlank
private String sortCode;
public AccountDetails() {}
public String getSortCode() {
return sortCode;
}
public void setSortCode(String sortCode) {
this.sortCode = sortCode;
}
public String getAccountNumber() {
return accountNumber;
}
public void setAccountNumber(String accountNumber) {
this.accountNumber = accountNumber;
}
}
#ControllerAdvice
public class ExceptionHandler extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatus status, WebRequest request) {
String errorMessage = ex.getBindingResult()
.getFieldErrors()
.stream()
.findFirst()
.map(FieldError::getDefaultMessage)
.get();
final BankDetailsValidationModel validationResult = new BankDetailsValidationModel(false, false, errorMessage, "");
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(validationResult);
}
}
This can be done by defining a custom validation at the class level, rather than at the field level.
An example:
You have a Person class containing the fields birthYear and deathYear. The business rule requires the birthYear to be non-null if the deathYear is non-null. Otherwise, both fields can be null; or the deathYear can be null if the birthYear is not null.
In this example, we have a relationship between the two fields - in your case, you do not. But that does not change the basic approach: there is one validation, with one message, summarizing the state of both fields.
The Person class:
#FirstOrBoth(firstField = "year of birth", secondField = "year of death", message = ValidationHandler.FIRST_OR_BOTH_MESSAGE)
public class Person {
private Integer birthYear;
private Integer deathYear;
[...the rest of the class...]
}
The #FirstOrBoth Annotation
The above class uses a custom annotation: #FirstOrBoth:
import java.lang.annotation.Target;
import java.lang.annotation.Retention;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import javax.validation.Constraint;
import javax.validation.Payload;
#Target(TYPE)
#Retention(RUNTIME)
#Constraint(validatedBy = FirstOrBothValidator.class)
public #interface FirstOrBoth {
String message() default "Please provide values in both fields, or only in the first (or leave both blank).";
String firstField();
String secondField();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
The FirstOrBothValidator Class
The above annotation uses a custom validator:
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class FirstOrBothValidator implements ConstraintValidator<FirstOrBoth, Object> {
#Override
public boolean isValid(Object obj, ConstraintValidatorContext cvc) {
Person person = (Person) obj;
// whatever validation logic you need goes here
// this is just for illustration:
return !(person.getBirthYear() == null && person.getDeathYear() != null);
}
}
As you already know, you can insert data values into your validation message using placeholders.
Final note: You mention that you can’t change the format of the error messages.
This approach assumes you can at least concatenate the two messages into the single message for the class-level validation.

Spring annotation method purpose

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
}
}
}

Categories

Resources