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:
values.remove(i)
in the initialize()
method of EnumValidatorImpl
, although elements of Set
are not indexed and Set<String> values
has generic type String
.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']
}