When writing custom constraints is it possible to have multiple annotations validated by one validator implementation. For example I have several annotations which stipulate different #size annotations but I want them all to point at the same validator class which does some global checking i.e. all must be match a certain regex. As far as I can see the implementation takes in one Annotation Type.
One annotation
#Target( { METHOD, FIELD, ANNOTATION_TYPE, TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = {UCNValidator.class})
#Documented
#Size(min = 9, max = 9, message = "{exactlength}")
public #interface UCN {
String message() default "{invalidFormat}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String fieldName() default "ucn";
}
The validator
public class UCNValidator implements ConstraintValidator<UCN, String>
{
private String pattern = "[a-zA-Z].*";
private String fieldName;
#Override
public void initialize( UCN constraintAnnotation )
{
this.fieldName = constraintAnnotation.fieldName();
}
#Override
public boolean isValid( String value, ConstraintValidatorContext constraintValidatorContext )
{
if ( value != null )
{
if ( !value.matches(pattern) )
{
//do some stuff
return false;
}
}
return true;
}
There doesn't seem to be a way to access other values from an object when validating one of its properties. The solution I use is to put the annotation on the class, then the validator will get the entire object in to validate against, and you can access just the info you need to perform the validation.
Here's one I wrote to compare two different properties of an object against each other:
#Target(TYPE)
#Retention(RUNTIME)
#Constraint(validatedBy = LessThanValidator.class)
#Documented
public #interface LessThan {
String message() default "{com.bullethq.constraints.LessThan}";
String bigValueProperty();
String littleValueProperty();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Then the validator class is:
public class LessThanValidator implements ConstraintValidator<LessThan, Object> {
private LessThan constraint;
public void initialize(LessThan constraintAnnotation) {
constraint = constraintAnnotation;
}
public boolean isValid(Object object, ConstraintValidatorContext cvc) {
Object bigValue = getValue(object, constraint.bigValueProperty());
Object littleValue = getValue(object, constraint.littleValueProperty());
// If one of the values is null, then we do not perform validation.
if (bigValue == null || littleValue == null) {
return true;
}
if (bigValue instanceof Comparable && littleValue instanceof Comparable) {
boolean valid = ((Comparable<Object>) bigValue).compareTo(littleValue) > 0;
if (!valid) {
// If the values are not valid, then build a custom violations which has the correct path in it.
cvc.buildConstraintViolationWithTemplate(cvc.getDefaultConstraintMessageTemplate())
.addNode(constraint.littleValueProperty())
.addConstraintViolation().disableDefaultConstraintViolation();
}
return valid;
}
throw new IllegalArgumentException("Properties " + constraint.bigValueProperty() + " and " + constraint.littleValueProperty() + " both need to be comparable in " + object.getClass());
}
}
The getValue() method is just a static method using reflection to get the value from the object.
Related
In my spring boot application I want to validate enum by custom value:
I have my DTO like following :
#Data
public class PayOrderDTO {
#NotNull
#EnumValidator(enumClass = TransactionMethod.class)
private TransactionMethod method;
}
And my enum validator annotation defined like bellow:
#Retention(RetentionPolicy.RUNTIME)
#Target({ElementType.FIELD, ElementType.METHOD})
#Constraint(validatedBy = EnumValidatorImpl.class)
public #interface EnumValidator {
String message() default "is invalid";
/**
* #return Specify group
*/
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* #return Specifies the enumeration type. The parameter value must be a value in this enumeration type
*/
Class<? extends EnumBase> enumClass();
/**
* #return Can it be null
*/
boolean nullable() default false;
/**
* #return Values to exclude
*/
int[] exclusion() default {};
}
This is the implementation of my enum validator annotation
public class EnumValidatorImpl implements ConstraintValidator<EnumValidator, EnumBase> {
private boolean nullable;
private Set<String> values;
#Override
public void initialize(EnumValidator constraintAnnotation) {
this.nullable = constraintAnnotation.nullable();
Class<? extends EnumBase> enumClass = constraintAnnotation.enumClass();
int[] exclusion = constraintAnnotation.exclusion();
values = new HashSet<>();
EnumBase[] enumConstants = enumClass.getEnumConstants();
for (EnumBase iEnum : enumConstants) {
values.add(iEnum.getValue());
}
if (exclusion.length > 0)
for (int i : exclusion) {
values.remove(i);
}
}
#Override
public boolean isValid(EnumBase param, ConstraintValidatorContext constraintValidatorContext) {
if (nullable && param == null) {
return true;
}
else if(param == null)
return false;
return values.contains(param.getValue());
}
}
this is my enum:
public enum TransactionMethod implements EnumBase {
CREDIT_CARD("creditcard"),
DEBIT_CARD("debitcard");
public String label;
TransactionMethod(String label) {
this.label = label;
}
#Override
public String getValue() {
return this.label;
}
#JsonCreator(mode = JsonCreator.Mode.DELEGATING)
public static TransactionMethod fromString(String value) {
return TransactionMethod.valueOf(value);
// return Arrays.stream(TransactionMethod.values())
// .filter(el -> el.getValue().equals(value))
// .findFirst()
// .orElseThrow(() -> {
// throw new IllegalArgumentException("Not valid method");
// });
}
}
when I'm sending my http request to that rest controller :
#RequiredArgsConstructor
#RestController
#RequestMapping("/orders")
#Validated
public class PaymentRestController {
public ResponseEntity<?> createPayment(
#Valid #RequestBody PayOrderDTO payOrderDTO
) {
return ResponseEntity.ok("Worked");
}
}
request example:
POST /orders/ HTTP/1.1
Host: store.test
Content-Type: application/json
Content-Length: 152
{
"method":"creditcard",
}
I'm expecting to get invalidation exception or error message defined in my enum validator, instead I get an exception in the console that contains :
JSON parse error: Cannot construct instance of `x.TransactionMethod`, problem: No enum constant x.TransactionMethod.creditcard
But if I sent this request :
POST /orders/ HTTP/1.1
Host: store.test
Content-Type: application/json
Content-Length: 152
{
"method":"CREDIT_CARD",
}
the application works normal
I want to validate enum using label instead of constant value of the enum, if it doesn't exists, a validation error will be thrown like :
HTTP 422 : field 'method' is not valid, expected values are ['creditcard','debitcard']
I tried some solutions as well like the convertor
public class TransactionMethodStringEnumConverter implements Converter<String, TransactionMethod> {
#Override
public TransactionMethod convert(String source) {
Optional<TransactionMethod> first = Arrays.stream(TransactionMethod.values()).filter(e -> e.label.equals(source)).findFirst();
return first.orElseThrow(() -> {
throw new IllegalArgumentException();
});
}
}
but seems like I does nothing.
I would really appreciate is someone has a good solution for this, Thank you 🙏
To deserialize an enum by label value you can use #JsonValue annotation on a getter:
public enum TransactionMethod implements EnumBase {
CREDIT_CARD("creditcard"),
DEBIT_CARD("debitcard");
public String label;
TransactionMethod(String label) {
this.label = label;
}
#Override
#JsonValue
public String getValue() {
return this.label;
}
}
How To Serialize and Deserialize Enums with Jackson
Also, pay attention to the facts:
You have values.remove(i) in the initialize() method of EnumValidatorImpl, although elements of Set are not indexed and Set<String> values has generic type String.
In your EnumValidator you can set nullable=true using boolean nullable(), but in PayOrderDTO you still check the method field for null using #NotNull, which can lead to an undesirable result.
EDIT:
You can define locale-specific messages with MessageSource, look at this article.
In your message.properties: invalid.transaction.method=is not valid, expected values are ['creditcard','debitcard']
Then add message to annotation:
#EnumValidator(enumClass = TransactionMethod.class, message = "{invalid.transaction.method}")
By failed validation catchMethodArgumentNotValidException in #ExceptionHandler method in #RestController or #RestControllerAdvice and format message as you want, e.g.:
#ExceptionHandler(MethodArgumentNotValidException.class)
#ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
public void handleException(MethodArgumentNotValidException e) {
String errorMessage = e.getBindingResult().getFieldErrors().stream()
.map(fieldError -> fieldError.getField() + " " + fieldError.getDefaultMessage())
.collect(Collectors.joining(";"));
LOG.error(e.getMessage()); // method is not valid, expected values are ['creditcard','debitcard']
}
I have next constraint annotation:
#Target({TYPE, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Constraint(validatedBy = PhoneOrEmailPresentedValidator.class)
#Documented
public #interface PhoneOrEmailPresented {
String message() default "Either phone or email should be presented";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
And the validator:
public class PhoneOrEmailPresentedValidator implements ConstraintValidator<PhoneOrEmailPresented, RequestDto> {
#Override
public boolean isValid(RequestDto value, ConstraintValidatorContext context) {
String email = value.getEmail();
String phone = value.getPhone();
return (email != null && !email.isEmpty()) ||
(phone != null && !phone.isEmpty());
}
#Override
public void initialize(PhoneOrEmailPresented constraintAnnotation) {
// NOP
}
}
I would like to use this annotation on another DTO which has the same fields that are presented in isValid() method.
Is it possible without extracting an interface for get methods?
As no way in validation API were found the issue were solved using reflection (with help of class org.apache.commons.lang3.reflect.FieldUtils, the same can be done with Java as well):
String email, phone;
try {
Field emailField = FieldUtils.getField(value.getClass(), "email", true);
Field phoneField = FieldUtils.getField(value.getClass(), "phone", true);
email = (String) emailField.get(value);
phone = (String) phoneField.get(value);
} catch (IllegalAccessException e) {
e.printStackTrace();
return false;
}
Implementing a custom constraint annotation, like #MySize requires me testing it with unit tests to see if it functions correctly:
public class MySizeTest {
#Test
public void noMinMax() {
Dummy dummy = new Dummy();
// some asserts or so
dummy.setMyField("");
dummy.setMyField(null);
dummy.setMyField("My text");
}
#Test
public void onlyMin() {
// change #MySize to have min: #MySize(min = 1)
... how?
... then test with some setMyField:
Dummy dummy = new Dummy();
// some asserts or so
dummy.setMyField("");
dummy.setMyField(null);
dummy.setMyField("My text");
}
#Test
public void onlyMax() {
// change #MySize to have max: #MySize(max = 50)
...
}
#Test
public void bothMinMax() {
// change #MySize to have min and max: #MySize(min = 1, max = 50)
...
}
private class Dummy {
#MySize()
String myField;
public String getMyField() {
return myField;
}
public void setMyField(String myField) {
this.myField = myField;
}
}
}
I assume this has to be done with reflection, but I have no idea how.
Basicly don't have to use reflection just create a Validator instance and use that for validating.
For examaple:
When the annotation is:
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = MyValidator.class)
public #interface MyAnnotation {
String message() default "Invalid value (it must be foo)";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
and the related validator is:
public class MyValidator implements ConstraintValidator<MyAnnotation, String> {
#Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
if (null == s) return true;
return "foo".equalsIgnoreCase(s);
}
}
Then the tests sould be like these:
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class MyValidatorTest {
private Validator validator;
#BeforeAll
void init() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
private static class TestObject {
#MyAnnotation
private String testField;
TestObject() {
this(null);
}
TestObject(String value) {
testField = value;
}
public String getTestField() {
return testField;
}
public void setTestField(String testField) {
this.testField = testField;
}
}
#Test
void shouldValidForNullValue() {
var obj = new TestObject();
var violations = validator.validate(obj); // Set<ConstraintViolation<TestObject>>
Assertions.assertTrue(violations.isEmpty(), String.format("Object should valid, but has %d violations", violations.size()));
}
#Test
void shouldValidForFooValue() {
var obj = new TestObject("foo");
var violations = validator.validate(obj); // Set<ConstraintViolation<TestObject>>
Assertions.assertTrue(violations.isEmpty(), String.format("Object should valid, but has %d violations", violations.size()));
}
#Test
void shouldInvalidForBarValue() {
var obj = new TestObject("bar");
var violations = validator.validate(obj); // Set<ConstraintViolation<TestObject>>
Assertions.assertEquals(1, violations.size());
}
}
Update (2020.05.21.) - Using attributes and AnnotationFactory
Based on comments I've updated my answer.
If you want to test only the validation logic then just create an Annotation instance and call the isValid method which is returns true or false
Hibernate Validator provides AnnotationFactory.create(...) method to make annotaion instance.
After that you can create an instance of your custom validator and call initialize and isValid methods in your test case.
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = MyHasAttributesValidator.class)
public #interface MyAnnotationHasAttributes {
String message() default "Invalid value (it must be foo)";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int attributeOne() default 10;
int attributeTwo() default 20;
}
related validator:
public class MyHasAttributesValidator implements ConstraintValidator<MyAnnotationHasAttributes, String> {
private MyAnnotationHasAttributes ann;
#Override
public void initialize(MyAnnotationHasAttributes constraintAnnotation) {
ann = constraintAnnotation;
}
#Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
if (null == s) return true;
return s.length() >= ann.attributeOne() && s.length() < ann.attributeTwo();
}
}
and the modified test (which has failing assertion):
public class HasAttributeValidatorTest {
private MyAnnotationHasAttributes createAnnotation(Integer one, Integer two) {
final Map<String, Object> attrs = new HashMap<>();
if (null != one) {
attrs.put("attributeOne", one);
}
if (null != two) {
attrs.put("attributeOne", two);
}
var desc = new AnnotationDescriptor.Builder<>(MyAnnotationHasAttributes.class, attrs).build();
return AnnotationFactory.create(desc);
}
#ParameterizedTest
#MethodSource("provideValues")
void testValidator(Integer one, Integer two, String input, boolean expected) {
MyAnnotationHasAttributes ann = createAnnotation(one, two);
MyHasAttributesValidator validator = new MyHasAttributesValidator();
validator.initialize(ann);
var result = validator.isValid(input, null);
Assertions.assertEquals(expected, result, String.format("Validation must be %s but found: %s with params: %d, %d, %s", expected, result, one, two, input));
}
private static Stream<Arguments> provideValues() {
return Stream.of(
Arguments.of(null, null, null, true),
Arguments.of(null, 20, "foo", true),
Arguments.of(null, null, RandomStringUtils.randomAlphabetic(30), false)
);
}
}
Limitations of this solution
Vendor lock
In this case your test using Hibernate Validator which is a specific implementation if the Bean Validation standards. Honestly I don't think it's a huge problem, because Hibernate Validator is the refecerence implementation and the most popular bean validation library. But technically it's a vendor lock.
Cross field validation is unavailable
This soulution works only in one-field situations. If you have e.g a cross-field validator (e.g. password and confirmPassword matching) this example won't fit.
Type independent validation needs more work
Like previously mentioned #Size annotation belongs to several different validator implementations based on type (primitives, collections, string, etc.).
Using this solution you always have to chose the certain validator manually and test it.
Only the isValid method can be tested
In this case you won't be able to test another things just the isValid method. I mean e.g. error message has expected format and parameters or something like this.
In sort, I know creating many different fields with different annotation attributes is boring but I strongly prefer that way because you can test everything you need about your validator.
I created an annotation in order to validate the value of a String field (like an enum) :
#Target({ElementType.METHOD, ElementType.FIELD})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = GenericStringSetValidator.class)
public #interface ValidStringValue {
String message() default "Must be a valid value. Found: ${validatedValue}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String[] values() default {};
}
And the GenericStringSetValidator used by the annotation :
public class GenericStringSetValidator implements ConstraintValidator<ValidStringValue, String> {
private Set<String> validValues;
#Override
public void initialize(ValidStringValue constraint) {
validValues = Arrays.stream(constraint.values())
.collect(toSet());
}
#Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
if (value == null) return false;
return validValues.contains(value);
}
}
I already use this in the following class whithout any trouble :
public class SampleClass{
private String id;
#ValidStringValue(values = {"IN_PROGRESS", "COMPLETED", "DELETED"})
private String status;
}
But I also want to use it in an other class with List<String> in order to validate each items of the List. Is it possible to configure my ValidStringValue interface to make it usable on a String and also a List<String> ?
public class SampleClassWithList{
private String id;
#ValidStringValue(values = {"A", "B", "C"})
private List<String> sampleList;
}
Thanks a lot for your help :)
I have a custom ConstraintValidator. What i want to do, is to add multiple constraint violations for the same property node, with the same message, and different dynamic payload. Is this possible?
Custom annotation:
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = CustomFieldConstraintValidator.class)
public #interface CustomFieldConstraint {
String message() default "message";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Custom constraint validator:
public class CustomFieldConstraintValidator implements ConstraintValidator<CustomFieldConstraint, Integer> {
#Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
var valid = true;
var hibernateConstraintValidatorContext = context.unwrap(HibernateConstraintValidatorContext.class);
hibernateConstraintValidatorContext.disableDefaultConstraintViolation();
if (value > 0) {
valid = false;
hibernateConstraintValidatorContext.withDynamicPayload(Map.of("must_be_less_than", 0))
.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
.addPropertyNode("field")
.addConstraintViolation();
}
if (value > 5) {
valid = false;
hibernateConstraintValidatorContext.withDynamicPayload(Map.of("must_be_less_than", 5))
.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
.addPropertyNode("field")
.addConstraintViolation();
}
return valid;
}
}
Validator test:
public class HibernateValidatorTest {
private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
#Test
void multipleFailuresForSameField() {
var foo = new Foo(10);
var constraintViolations = validator.validate(foo);
assertEquals(2, constraintViolations.size());
}
public static class Foo {
#CustomFieldConstraint
private int field;
public Foo(int field) {
this.field = field;
}
}
}
The test is failing with expected: <2> but was: <1>.
You have this issue because ConstraintViolation are returned as a set and, by design, the dynamic payload is not included in the equals() method. See https://github.com/hibernate/hibernate-validator/blob/master/engine/src/main/java/org/hibernate/validator/internal/engine/ConstraintViolationImpl.java#L251 .
Thus your 2 constraints are considered equal at the Set level.
You need to change the way you do things and enrich the payload with both information instead.