16
votes

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;
}
4
have you tried with @Nonnull ? - Suresh Atta
Here is a very nice and to the point tutorial on this subject: Validations for Enum Types - informatik01

4 Answers

40
votes

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

13
votes

@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.

7
votes

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.

2
votes

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+.