NarasuOo
NarasuOo

Reputation: 465

How to validate enum in DTO?

In the domain model object I have the following field:

private TermStatus termStatus;

TermStatus is an enum:

public enum TermStatus {
    NONE,
    SUCCESS,
    FAIL
}

In the DTO, I have the same field as in the domain object. The question is, how can I validate the passed value? If the API client now passes an incorrect string with the enum value as a parameter (for example, nOnE), it will not receive any information about the error, only the status 400 Bad Request. Is it possible to validate it like this, for example, in the case of javax.validation annotations like @NotBlank, @Size, where in case of an error it will at least be clear what it is. There was an idea to make a separate mapping for this, for example "items/1/complete-term" instead of direct enum transmission, so that in this case the server itself would set the SUCCESS value to the termStatus field. But as far as I know, these things don't look very good in REST API, so I need your ideas

Upvotes: 8

Views: 24437

Answers (4)

Geeth
Geeth

Reputation: 554

You should use String data type for termStatus. Because of client sends String value for this. Then you have to create Custom validation constraints to fix this as below.

ValueOfEnum

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = ValueOfEnumValidator.class)
public @interface ValueOfEnum
{
    Class<? extends Enum<?>> enumClass();

    String message() default "";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

ValueOfEnumValidator

public class ValueOfEnumValidator implements ConstraintValidator<ValueOfEnum, CharSequence>
{
    private List<String> acceptedValues;

    @Override
    public void initialize(ValueOfEnum annotation)
    {
        acceptedValues = Stream.of(annotation.enumClass().getEnumConstants())
            .map(Enum::name)
            .collect(Collectors.toList());
    }

    @Override
    public boolean isValid(CharSequence value, ConstraintValidatorContext context)
    {
        if (value == null) {
            return true;
        }
        return acceptedValues.contains(value.toString());
    }
}

Now you can @ValueOfEnum annotation for your domain model. Then add @Validated annotation in front of your controller class domain object.

@ValueOfEnum(enumClass = TermStatus.class, message = "Invalid Term Status")
private String termStatus;

Upvotes: 0

Paweł Lenczewski
Paweł Lenczewski

Reputation: 126

Instead of validating enum directly, you could check whether String is valid for specific enum. To achieve such an effect you could create your own enum validation annotation.

@Documented
@Constraint(validatedBy = EnumValidatorConstraint.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@NotNull
public @interface EnumValidator {

Class<? extends Enum<?>> enum();
String message() default "must be any of enum {enum}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

Then you need to implement validator to check whether String exist as a part of this enum.

public class EnumValidatorConstraint implements ConstraintValidator<EnumValidator, String> {

Set<String> values;

@Override
public void initialize(EnumValidator constraintAnnotation) {
    values = Stream.of(constraintAnnotation.enumClass().getEnumConstants())
            .map(Enum::name)
            .collect(Collectors.toSet());
}

@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
    return values.contains(value);
}
}

Lastly, you need to annotate your field with @EnumValidator.

@EnumValidator(enum = TermStatus.class)
private String termStatus;

In case of not matching String a MethodArgumentNotValidException will be thrown. Same as for @NotNull or other constraint validation.

Upvotes: 7

Aritra Paul
Aritra Paul

Reputation: 874

You can make a utility method inside your enum like below private String text;

TermStatus(String text) {
    this.text = text;
}
public static TermStatus fromText(String text) {
        return Arrays.stream(values())
                .filter(bl -> bl.text.equalsIgnoreCase(text))
                .findFirst()
                .orElse(null);
    }

And set value in dto like below

dto.setTermStatus(TermStatus.fromText(passedValue))
if(dto.getTermStatus()== null)
throw new Exception("Your message");

Hope this helps!

Upvotes: 1

JekBP
JekBP

Reputation: 81

Sounds like you need to implement your own response after validation and tell the API client that the data in your received DTO is invalid and return message with the actual received value (nOnE in your case) and maybe the list of your valid values (if that's not gonna be a security concern). Also, I think the ideal http status for your response would be 422 instead of a generic 400 Bad Request.

For your actual validation implementation, I think you can just directly compare the converted value from DTO to ENUM of the data you received from the API client against your ENUM values in the back-end. If equals to any of the ENUM values, then it's a valid request (200) else, 422.

Hope this helps!

Upvotes: 0

Related Questions