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']
}
Related
I have a url like http://localhost:8080?scope=openid%20profile
I want to transfer it to http://localhost:8080?scope=openid&scope=profile
so that the following endpoint could accept it.
// Scope.java
public enum Scope {
OPENID,
PROFILE;
public static Scope fromString(String value) {
return valueOf(value.toUpperCase());
}
}
// AuthorizationEndpoint.java
#GET
#Path("authorize")
public Response authorize(#Valid #NotEmpty #QueryParam("scope") Set<Scope> scope) {
...
}
I tried to add a filter like below, but the map of request params is unmodifiable !
// Split.java
public #interface Split {
String value();
}
// AuthorizationEndpoint.java
#GET
#Path("authorize")
// add #Split(" ")
public Response authorize(#Valid #NotEmpty #QueryParam("scope") #Split(" ") Set<Scope> scope)
{
...
}
// SplitFilter.java
#Provider
class SplitFilter implements ContainerRequestFilter {
private final ResourceInfo resourceInfo;
SplitFilter(ResourceInfo resourceInfo) {
this.resourceInfo = resourceInfo;
}
#Override
public void filter(ContainerRequestContext requestContext) {
for (var parameter: resourceInfo.getResourceMethod().getParameters()) {
var queryParam = parameter.getAnnotation(QueryParam.class);
if (queryParam == null) continue;
var split = parameter.getAnnotation(Split.class);
if (split == null) continue;
var queryName = queryParam.value();
// Note: queryParams is unmodifiable!!!
var queryParams = requestContext.getUriInfo().getQueryParameters();
var originQueryValues = queryParams.get(queryName);
if (originQueryValues.size() != 1) {
throw new IllegalStateException("Incorrect size of query values: " + originQueryValues + ", expect only 1.");
}
var splitQueryValue = originQueryValues.get(0).split(split.value());
// Error: originQueryValues is unmodifiable!!!
originQueryValues.clear();
originQueryValues.addAll(Arrays.asList(splitQueryValue));
}
}
}
So is there a proper way to modify the request params in filters or other inteceptors?
I also tried to make Set<Scope> a single class that recieves a String value as constructor param,
but how to get the converter that converts a String value to a instance of Scope instead of calling Scope.fromString?
I think you can't use a filter for that, exactly because a filter is not allowed to change the params. You could use a param converter, roughly something like this:
#Provider
public class ScopeConverterProvider implements ParamConverterProvider {
#Override
public <T> ParamConverter<T> getConverter(Class<T> rawType, Type genericType, Annotation[] annotations) {
if(rawType == Set.class) {
for (Annotation annotation : annotations) {
if (annotation.annotationType().equals(Split.class)) {
return (ParamConverter<T>) new ScopeConverter(((Split) annotation).value());
}
}
return (ParamConverter<T>) new ScopeConverter(" ");// default if there's no #Split annotation
}
return null;
}
}
public class ScopeConverter implements ParamConverter<Set<Scope>> {
private final String splitCharacter;
public ScopeConverter(String splitCharacter) {
this.splitCharacter = splitCharacter;
}
#Override
public Set<Scope> fromString(String value) {
// strip the [] that the value gets automatically wrapped in,
// because the query param is a set
value = value.substring(1, value.length() - 1);
String[] splits = value.split(splitCharacter);
HashSet<Scope> scopes = new HashSet<>();
for (String split : splits) {
scopes.add(Scope.fromString(split));
}
return scopes;
}
#Override
public String toString(Set<Scope> value) {
return null; // unused
}
}
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 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.
I am trying to validate Double in the request of my dropwizard application. Below is the code for same
import javax.validation.constraints.NotNull;
#PATH("/path")
public class MyResource {
#GET
public Response addNumber( #QueryParam("first") #NotNull Double first, #QueryParam("second") #NotNull Double second) {
return Response.ok("added").build();
}
}
I get the below error when querying the resource localhost/path?first=46.76&second=67.56
{
"errors": [
"query param first may not be null",
"query param second may not be null"
]
}
If we remove the #NotNull from the resource the resource responds successfully.
Why does #NotNull say it should be not null although the values are present?
I had the same requirement. So implemented custom validator.
#Retention(RUNTIME)
#Target({ElementType.FIELD})
#Constraint(validatedBy = NotZeroDoubleValidatorImpl.class)
public #interface NotZeroDoubleValidator {
String message() default "{Double value is required}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* Field to validate against 0.
*/
double field() default 0;
}
And
public class NotZeroDoubleValidatorImpl implements ConstraintValidator<NotZeroDoubleValidator, Double> {
#Override
public void initialize(final NotZeroDoubleValidator notZeroDoubleValidator) {
}
#Override
public boolean isValid(final Double value, final ConstraintValidatorContext context) {
return Double.compare(value, 0) != 0;
}
}
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.