1

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 🙏

1
  • 1
    Adding Validator on Enum type of object in DTO is not working for me but when I change the type to String and validate String ConstraintValidator its working. Can you please confirm how you did that? I can see you have created a Base Interface for your Enum. Commented Apr 6, 2023 at 11:49

1 Answer 1

1

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:

  1. 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.
  2. 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']
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.