I'm trying to make a custom java validation annotation and returns me
Request processing failed; nested exception is javax.validation.ConstraintDeclarationException: HV000144: Cross parameter constraint com.my.company.CustomConstraint is illegally placed on field 'private java.util.List com.my.company.ElementOfTheList'."
the code is really naive
#Documented
#Retention(RUNTIME)
#Target({ FIELD, METHOD})
#Constraint(validatedBy = ConstraintValidation.class)
public #interface CustomConstraint {
String message() default "this is the default message";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
#SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class ConstraintValidationimplements ConstraintValidator<CustomConstraint , List<ElementOfTheList>> {
public boolean isValid(List<ElementOfTheList> value, ConstraintValidatorContext context) {
System.out.println("only a sysout to test");
return true;
}
}
And in the rest object model
#JsonProperty("ElementOfTheList")
#Valid
#NotNull(message ="not null message")
#NotEmpty(message = "not empty message")
#CustomConstraint
private List<ElementOfTheList> list = null;
change
#SupportedValidationTarget(ValidationTarget.PARAMETERS)
to
#SupportedValidationTarget(ValidationTarget.ANNOTATED_ELEMENT)
since you want to validate and element (here is List list)
and not the parameters of a method or a constructor
Related
I have a custom annotation called Matches which has a default value for message. This is essentially a class-level custom constraint validator but that's not part of the problem.
#Constraint(validatedBy = MatchesValidator.class)
#Documented
#Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Repeatable(Matches.List.class)
public #interface Matches {
String field();
String otherField();
String message() default "{com.example.api.validation.constraints.Matches.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
#Documented
#Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
#Retention(RetentionPolicy.RUNTIME)
#interface List {
Matches[] value();
}
}
This is a simple constraint that will be applied on a class to validate if two fields have the same value:
#Matches(field = "password", otherField = "confirmPassword")
class UserRegistration {
private String password;
private String confirmPassword;
// Getters
}
I'm writing a test using JUnit and Mockito and and I'm trying to mock the Matches annotation:
#Test
void isValid_whenFieldsDoNotMatch_thenReturnsFalse() {
Matches invalidPropertyMatches = Mockito.mock(Matches.class);
when(invalidPropertyMatches.field()).thenReturn("password");
when(invalidPropertyMatches.field()).thenReturn("confirmPassword");
when(invalidPropertyMatches.message()).thenCallRealMethod(); // This throws
}
I want that when the Matches.message() value is called, I get back the default defined value "{com.example.api.validation.constraints.Matches.message}", so I added:
when(invalidPropertyMatches.message()).thenCallRealMethod();
However, this throws the following exception:
org.mockito.exceptions.base.MockitoException:
Cannot call abstract real method on java object!
Calling real methods is only possible when mocking non abstract method.
//correct example:
when(mockOfConcreteClass.nonAbstractMethod()).thenCallRealMethod();
It essentially says that Matches.message() is abstract and cannot be called. Are annotation properties in Java abstract? Is there a way to fix this?
A custom enum validator annotation interface:
#Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
#Retention(RUNTIME)
#Documented
#Constraint(validatedBy = PanTypeSubSetValidator.class)
public #interface PanTypeSubset {
PanType[] anyOf();
String message() default "must be any of {anyOf}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
and the actual implementation:
public class PanTypeSubSetValidator implements ConstraintValidator<PanTypeSubset, PanType> {
private PanType[] subset;
#Override
public void initialize(PanTypeSubset constraint) {
this.subset = constraint.anyOf();
}
#Override
public boolean isValid(PanType value, ConstraintValidatorContext context) {
return value == null || Arrays.asList(subset).contains(value);
}
}
and the usage inside a request DTO:
#SuperBuilder
#Data
#NoArgsConstructor
public class PanBaseRequestDto {
#NotNull(message = "'PANTYPE' cannot be empty or null")
#PanTypeSubset(anyOf = {PanType.PAN, PanType.TOKEN}, message = "yesssss")
private PanType panType;
}
The problem is that this annotation never seems to be triggered. I get another exception kick in in the #RestControllerAdvice DefaultExceptionHandler implementation before this actual validation:
Handling generic exception: (Invalid JSON input: Cannot deserialize value of type `...pantoken.PanType` from String "PAN1": not one of the values accepted for Enum class: [TOKEN, PAN]; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `...pantoken.PanType` from String "PAN1": not one of the values accepted for Enum class: [TOKEN, PAN]
Solved it by creating a custom #JsonCreator function inside the ENUM class. Not the best approach, as we loose the value that user has submitted when displaying error to the end client, but it's ok for me.
#JsonCreator
public static PanType create(String value) {
if (Objects.isNull(value)) {
return null;
}
return Arrays.stream(PanType.values())
.filter(v -> value.equals(v.getType()))
.findFirst()
.orElse(PanType.UNKNOWN);
}
I receive list of domain object ids deserialized from JSON client request body:
#JsonProperty("workgroups")
private List<WorkgroupId> workgroupIds = new ArrayList<>();
I need to validate these ids in org.springframework.validation.Validator.
for (WorkgroupId workgroupId : project.getWorkgroupIds()) {
if (!domainObjectTools.doesWorkgroupExist(workgroupId)) {
// reject this invalid value here...
}
}
Question
How to reject invalid value in org.springframework.validation.Errors?
Validator interface:
#Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE })
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = { CustomValidatorValidator.class })
#Documented
public #interface CustomValidator{
String message() default "Put here your default message";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
ValidatorImplementation:
public final class CustomValidatorValidator implements ConstraintValidator<CustomValidator, List<WorkgroupId>> {
#Override
public void initialize(CustomValidator constraintAnnotation) {
}
#Override
public boolean isValid(List<WorkgroupId> yourlist, ConstraintValidatorContext context) {
if (yourlist== null)
return true;
else
return yourlist.stream().anyMatch(s -> /* filter here as you want */);
}
}
Notice we return true if the field is null, I do it like this because I just put #NotNull constraint if I need not to be null so I have more control over the constraint.
Finally:
#JsonProperty("workgroups")
#CustomValidator
private List<WorkgroupId> workgroupIds = new ArrayList<>();
P.S: I don't understand why you initialize the list in this last code. If that's a field you're supposed to receive through the request then you don't need to initialize it, the json deserializer will initialize it with the incoming field in the json.
You can use reject() and/or rejectValue() along with the field/error code/defaultMessage or along with a custom validator to reject the values.
I'm trying to implement a custom annotation to validate my fields. The idea is that the validation fails whenever the annotated field is null. Something like this.
#RequiredProperty
public abstract Person getPerson();
Now if this returns a null Person, I'd like the validation to fail (ideally with a custom message "Person field is null").
I tried to do it like this.
#Documented
#Constraint(validatedBy = RequiredPropertyValidator.class)
#Retention(RetentionPolicy.RUNTIME)
#Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE})
#ReportAsSingleViolation
public #interface RequiredProperty {
String message() default "{javax.validation.constraints.RequiredProperty.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
And validator.
public class RequiredPropertyValidator implements ConstraintValidator<RequiredProperty, Object> {
#Override
public void initialize(RequiredProperty constraintAnnotation) {
}
#Override
public boolean isValid(Object property, ConstraintValidatorContext context) {
return property != null;
}
}
However this won't work. It doesn't validate at all. Object property is never even passed to the isValid method. Any advice on how to get it working?
UPDATE
Removing the empty initialize method got it working. However, I'm not sure how to create a custom error message that the "Person" field is null. Is that possible?
I created a custom message in .properties file, but this is just a static message, and I'd like to capture the actual field from runtime.
I want to create custom email validator using annotations. This solution was useful while creating the validator.
Here's the annotation :
#Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = {CommonsEmailValidator.class})
#Documented
#ReportAsSingleViolation
public #interface ExEmailValidator {
String message() default " {org.hibernate.validator.constraints.Email.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
#Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface List {
ExEmailValidator[] value();
}
}
And here's the class CommonsEmailValidator :
public class CommonsEmailValidator implements ConstraintValidator<ExEmailValidator, String> {
private static final boolean ALLOW_LOCAL = false;
private EmailValidator realValidator = EmailValidator.getInstance(ALLOW_LOCAL);
#Override
public void initialize(ExEmailValidator email) {
// TODO Auto-generated method stub
}
#Override
public boolean isValid(String email, ConstraintValidatorContext constraintValidatorContext) {
if( email == null ) return true;
return realValidator.isValid(email);
}
}
I run the project, but, when I click submit on the registration form, while the email format is not valid I have the following exception :
Request processing failed; nested exception is javax.validation.ConstraintViolationException: Validation failed for classes [...] during persist time for groups [javax.validation.groups.Default, ]
the exception was as excpected, But instead of showing a 500 error, I would like to see an error message explaning what's wrong.
So, I think I have to use ConstraintValidatorContext to define custom error messages. But, I don't know how to do it.
Any ideas please ?
You should use #ControllerAdvice and handle validation exceptions there with #ExceptionHandler(MethodArgumentNotValidException.class) for example.