List not honoring #NotEmpty annotation - java

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();
}

Related

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

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
}

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 JSR-303 ConstraintValidator not support list.add()

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( ... ) ).

How to pass more parameters to spring validate method

I have extension of org.springframework.validation.Validator.
public class MyValidator implements Validator {
#Override
public void validate(Object target, Errors errors) {
...
}
}
My goal is to pass more than one target to method.
I don't like idea with overload validate method because it smells as bad code:
validate(Object target1, Object target1, Errors errors) or creating map with needed targets.
It will be good to know better approach regarding this case.
I did not try the following code, but it demonstrates a basic idea how one field of the bean could be verified against the other. Hopefully, it will help you
Let's say you have the following form bean
public class MyForm {
private String id;
private List<String> oldIds;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public List<String> getOldIds() {
return oldIds;
}
public void setOldIds(List<String> oldIds) {
this.oldIds = oldIds;
}
}
and the id property has to be validated against the oldIds object (if i did understand your requirements correctly). To achieve it your need to create a constraint and mark your bean. So, the first is the constraint interface
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import javax.validation.Constraint;
import javax.validation.Payload;
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = MyConstraintValidator.class)
#Documented
public #interface MyConstraint {
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String[] value();
}
next, you need to implement the constraint validator class:
import java.util.List;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.apache.commons.beanutils.PropertyUtils;
public class MyConstraintValidator implements
ConstraintValidator<MyConstraint, Object> {
private String firstAttribute;
private String secondAttribute;
#Override
public void initialize(final MyConstraint constraintAnnotation) {
firstAttribute = constraintAnnotation.value()[0];
secondAttribute = constraintAnnotation.value()[1];
}
#Override
public boolean isValid(final Object object,
final ConstraintValidatorContext constraintContext) {
try {
final String id = (String) PropertyUtils.getProperty(object,
firstAttribute);
List<String> oldIds = (List<String>) PropertyUtils.getProperty(
object, secondAttribute);
// do your validation
return true;
} catch (final Exception e) {
throw new IllegalArgumentException(e);
}
}
}
finally, apply the created constraint to the form bean
#MyConstraint(value = { "id", "oldIds" })
public class MyForm {
// the code
}
For now, your mark your bean with the #Valid annotation from the javax.validation package or feed it to the validator object
We use a target bean which holds all the data which need to be validated. Something like
private static final class ParamsBean {
String id;
List<String> oldIds;
}
Then we simply cast the object. It's the cleanest possible solution imo, as it does not use generic Map or List of unknown objects (though the casting still is not nice).
i faced with a similar situation where i need to pass more arguments to the validate method so i came up with a idea of my own.in my case i wanted a String to be passed to this method
validate method implemented in the following classes CustomValidatorBean, LocalValidatorFactoryBean, OptionalValidatorFactoryBean, SpringValidatorAdapter
I extended the CustomValidatorBean and called the validate method in super class and it is working perfectly
import javax.validation.Validator;`
import org.apache.commons.lang3.StringUtils;`
import org.springframework.validation.Errors;`
importorg.springframework.validation.beanvalidation.CustomValidatorBean;`
public class MyValidator extends CustomValidatorBean {`
public void myvalidate(Object target,Errors errors,String flag,Profile profile)
{
super.validate(target,errors);
if(StringUtils.isEmpty(profile.name())){
errors.rejectValue("name", "NotBlank.profilereg.name", new Object[] { "name" }, "Missing Required Fields");
}
}
}

Categories

Resources