20

How can I use hibernate annotations to validate an enum member field? The following does not work:

enum UserRole {
   USER, ADMIN;
}

class User {
   @NotBlank //HV000030: No validator could be found for type: UserRole.
   UserRole userRole;
}
membersound
  • 81,582
  • 193
  • 585
  • 1,120

4 Answers4

42

Note you can also create a validator to check a String is part of an enumeration.

public enum UserType { PERSON, COMPANY }

@NotNull
@StringEnumeration(enumClass = UserCivility.class)
private String title;

@Documented
@Constraint(validatedBy = StringEnumerationValidator.class)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, PARAMETER, CONSTRUCTOR })
@Retention(RUNTIME)
public @interface StringEnumeration {

  String message() default "{com.xxx.bean.validation.constraints.StringEnumeration.message}";
  Class<?>[] groups() default {};
  Class<? extends Payload>[] payload() default {};

  Class<? extends Enum<?>> enumClass();

}

public class StringEnumerationValidator implements ConstraintValidator<StringEnumeration, String> {

  private Set<String> AVAILABLE_ENUM_NAMES;

  @Override
  public void initialize(StringEnumeration stringEnumeration) {
    Class<? extends Enum<?>> enumSelected = stringEnumeration.enumClass();
    //Set<? extends Enum<?>> enumInstances = EnumSet.allOf(enumSelected);
    Set<? extends Enum<?>> enumInstances = Sets.newHashSet(enumSelected.getEnumConstants());
    AVAILABLE_ENUM_NAMES = FluentIterable
            .from(enumInstances)
            .transform(PrimitiveGuavaFunctions.ENUM_TO_NAME)
            .toSet();
  }

  @Override
  public boolean isValid(String value, ConstraintValidatorContext context) {
    if ( value == null ) {
      return true;
    } else {
      return AVAILABLE_ENUM_NAMES.contains(value);
    }
  }

}

This is nice because you don't loose the information of the "wrong value". You can get a message like

The value "someBadUserType" is not a valid UserType. Valid UserType values are: PERSON, COMPANY


Edit

For those who want a non-Guava version it should work with something like:

public class StringEnumerationValidator implements ConstraintValidator<StringEnumeration, String> {

  private Set<String> AVAILABLE_ENUM_NAMES;

  public static Set<String> getNamesSet(Class<? extends Enum<?>> e) {
     Enum<?>[] enums = e.getEnumConstants();
     String[] names = new String[enums.length];
     for (int i = 0; i < enums.length; i++) {
         names[i] = enums[i].name();
     }
     Set<String> mySet = new HashSet<String>(Arrays.asList(names));
     return mySet;
  }

  @Override
  public void initialize(StringEnumeration stringEnumeration) {
    Class<? extends Enum<?>> enumSelected = stringEnumeration.enumClass();
    AVAILABLE_ENUM_NAMES = getNamesSet(enumSelected);
  }

  @Override
  public boolean isValid(String value, ConstraintValidatorContext context) {
    if ( value == null ) {
      return true;
    } else {
      return AVAILABLE_ENUM_NAMES.contains(value);
    }
  }

}

And to customize the error message and display the appropriate values, check this: https://stackoverflow.com/a/19833921/82609

Community
  • 1
  • 1
Sebastien Lorber
  • 89,644
  • 67
  • 288
  • 419
  • How ould this have to be written without the guava functions? – membersound Oct 31 '14 at 11:11
  • 1
    @membersound really there is no complexity at all doing this. You just have to transform an array of enum (`enumSelected.getEnumConstants()` to a Set of enum names (`AVAILABLE_ENUM_NAMES`) and you'll easily find tutorials to do that with Java :) see here http://stackoverflow.com/questions/13783295/getting-all-names-in-an-enum-as-a-string – Sebastien Lorber Oct 31 '14 at 12:52
  • OK sorry, probably I misunderstood: I thought it would be possible to create a generic default error message, and inside print all the generic enum values., – membersound Oct 31 '14 at 13:27
  • 1
    @membersound yes you understand correctly: it is possible to print all the available enum values with my code. It has nothing to do with Guava, Guava is just some utility I used to achieve that but you can also with plan old Java. I've updated my answer – Sebastien Lorber Oct 31 '14 at 13:51
  • What about if you wanted the field, "title" in the original post, to actually be of the enum type? Why not use the enum as the field type? That's the situation I have now, I can't figure out how to validate that the string passed in my JSON is one of the enum values and catch the error properly like other Spring/Hibernate validation errors. Any suggestions? – Kevin M Jan 16 '17 at 17:09
  • @KevinM I'm not a Java dev anymore so I'll only give you my idea: the class you use to validate is not supposed to contain non-primitive types like Enums. If you added an enum here, then there would be nothing to validate because it would already be validated. You have 2 choices: validate in your json parser and map to an enum (like you) or not validate, map to a string, and then transform the valid POJO (with strings) to a valid typed POJO (with enums) – Sebastien Lorber Jan 16 '17 at 20:34
  • Thanks, I'm having a feeling I might have to go with Strings. The problem with relying on the java enum "validation" is that in Spring, it triggers an HttpMessageNotReadableException type of exception, which does not give you the same type of information (field that caused the error, etc) as if it were a Spring ConstraintValidator type of exception. Thanks! – Kevin M Jan 16 '17 at 22:37
  • @KevinM I don't know where come your `HttpMessageNotReadableException` but if you can customize the parser you registered in Spring it can probably give you better error messages. I know I used to customize my Gson deserialization process for handling that – Sebastien Lorber Jan 17 '17 at 10:03
  • @SebastienLorber I love this solution! But how can i change the validation error message to include what values are valid? – Robin Jonsson May 17 '17 at 14:04
  • @RobinJonsson I don't use validation anymore, but maybe you can try with `@StringEnumeration(enumClass = UserCivility.class, message="my custom message key")` – Sebastien Lorber May 17 '17 at 14:53
  • wtf is UserCivility.class and how is it construct – amdev Jun 02 '17 at 12:00
13

@NotBlank

Validate that the annotated string is not null or empty. The difference to NotEmpty is that trailing whitespaces are getting ignored.

Where as UserRole is not String and an object Use @NotNull

The annotated element must not be null. Accepts any type.

Suresh Atta
  • 120,458
  • 37
  • 198
  • 307
8

I suppose a more closely related to Sebastien's answer above with fewer lines of code and makes use of EnumSet.allOf in the expense of a rawtypes warning

Enums setup

public enum FuelTypeEnum {DIESEL, PETROL, ELECTRIC, HYBRID, ...}; 
public enum BodyTypeEnum {VAN, COUPE, MUV, JEEP, ...}; 

Annotation setup

@Target(ElementType.FIELD) //METHOD, CONSTRUCTOR, etc.
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumValidator.class)
public @interface ValidateEnum {
    String message() default "{com.xxx.yyy.ValidateEnum.message}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    Class<? extends Enum<?>> targetClassType();
}

Validator setup

public class EnumValidator implements ConstraintValidator<ValidateEnum, String> {
    private Set<String> allowedValues;

    @SuppressWarnings({ "unchecked", "rawtypes" })
    @Override
    public void initialize(ValidateEnum targetEnum) {
        Class<? extends Enum> enumSelected = targetEnum.targetClassType();
        allowedValues = (Set<String>) EnumSet.allOf(enumSelected).stream().map(e -> ((Enum<? extends Enum<?>>) e).name())
                .collect(Collectors.toSet());
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return value == null || allowedValues.contains(value)? true : false; 
    }
} 

Now go ahead and annotate your fields as follow

@ValidateEnum(targetClassType = FuelTypeEnum.class, message = "Please select ...." 
private String fuelType; 

@ValidateEnum(targetClassType = BodyTypeEnum.class, message = "Please select ...." 
private String bodyType; 

The above assumes you have the Hibernate Validator setup and working with default annotation.

Raf
  • 7,505
  • 1
  • 42
  • 59
  • I have shared a non-hibernate validator solution in here https://stackoverflow.com/a/59440957/1225551 that makes use of `Apache's EnumUtils` – Raf Dec 22 '19 at 01:01
2

Often times, attempting to convert to an enum is not just by name (which is the default behavior with valueOf method). For example, what if you have enums representing DayOfWeek and you want an integer to be converted to a DayOfWeek? To do that, I created the following annotation:

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Constraint(validatedBy = {ValidEnumValueValidator.class})
public @interface ValidEnumValue {
    String message() default "invalidParam";

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

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

    Class<? extends Enum<?>> value();

    String enumMethod() default "name";

    String stringMethod() default "toString";
}
public class ValidEnumValueValidator implements ConstraintValidator<ValidEnumValue, String> {
    Class<? extends Enum<?>> enumClass;
    String enumMethod;
    String stringMethod;

    @Override
    public void initialize(ValidEnumValue annotation) {
        this.enumClass = annotation.value();
        this.enumMethod = annotation.enumMethod();
        this.stringMethod = annotation.stringMethod();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        Enum<?>[] enums = enumClass.getEnumConstants();
        Method method = ReflectionUtils.findMethod(enumClass, enumMethod);

        return Objects.nonNull(enums) && Arrays.stream(enums)
                .map(en -> ReflectionUtils.invokeMethod(method, en))
                .anyMatch(en -> {
                    Method m = ReflectionUtils.findMethod(String.class, stringMethod);
                    Object o = ReflectionUtils.invokeMethod(m, value);

                    return Objects.equals(o, en);
                });
    }
}

You'd use it as follows:

public enum TestEnum {
        A("test");

        TestEnum(String s) {
            this.val = s;
        }

        private String val;

        public String getValue() {
            return this.val;
        }
    }
public static class Testee {
        @ValidEnumValue(value = TestEnum.class, enumMethod = "getValue", stringMethod = "toLowerCase")
        private String testEnum;
    }

Above implementation uses ReflectionUtils from Spring framework and Java 8+.

Abhijit Sarkar
  • 21,927
  • 20
  • 110
  • 219