Java String validation using enum values and annotation

101,286

Solution 1

This is what I did.

Annotation

public @interface ValidateString {

    String[] acceptedValues();

    String message() default "{uk.dds.ideskos.validator.ValidateString.message}";

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

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

Validation Class

public class StringValidator implements ConstraintValidator<ValidateString, String>{

    private List<String> valueList;

    @Override
    public void initialize(ValidateString constraintAnnotation) {
        valueList = new ArrayList<String>();
        for(String val : constraintAnnotation.acceptedValues()) {
            valueList.add(val.toUpperCase());
        }
    }

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

}

And i used it like

@ValidateString(acceptedValues={"Integer", "String"}, message="Invalid dataType")
String dataType;

Long maxValue;
Long minValue;

Now I need to figure out how to implement conditional check ie. if String then maxValue and minValue should be null or Zero..

Any ideas?

Solution 2

So here is the code being using Spring validation and works great for me. Full code is given below.


@EnumValidator annotation definition:

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;
import javax.validation.constraints.NotNull;

@Documented
@Constraint(validatedBy = EnumValidatorImpl.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@NotNull(message = "Value cannot be null")
@ReportAsSingleViolation
public @interface EnumValidator {

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

  String message() default "Value is not valid";

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

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

}


Implementation of the above class:

import java.util.ArrayList;
import java.util.List;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class EnumValidatorImpl implements ConstraintValidator<EnumValidator, String> {

    List<String> valueList = null;

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

    @Override
    public void initialize(EnumValidator constraintAnnotation) {
        valueList = new ArrayList<String>();
        Class<? extends Enum<?>> enumClass = constraintAnnotation.enumClazz();

        @SuppressWarnings("rawtypes")
        Enum[] enumValArr = enumClass.getEnumConstants();

        for (@SuppressWarnings("rawtypes") Enum enumVal : enumValArr) {
            valueList.add(enumVal.toString().toUpperCase());
        }
    }

}


Usage of the above annotation is very simple

 @JsonProperty("lead_id")
 @EnumValidator(
     enumClazz = DefaultEnum.class,
     message = "This error is coming from the enum class",
     groups = {Group1.class}
 )
 private String leadId;

Solution 3

Ditch the String representation, and do a real enum.

public enum DataType {
   STRING,
   BOOLEAN,
   INTEGER;
}

That way you avoid ever having to do string comparison of the previous String dataType variable to determine if it is in the enum. As an aside, it also makes it impossible to assign a non-valid value to the member variable dataType and since enums are guaranteed to be singletons within the class loader, it also saves on memory footprint.

It's worth the effort to change your code to use enums. However, assuming that you can't, you can at least change the annotation to use enums.

@ValidateString(DataType.STRING) String dataType;

and that way your ValidateString annotation at least gets to benefit from enums, even if the rest of the code doesn't.

Now on the extremely rare chance that you can't use an enumeration at all, you can set static public integers, which map each accepted value.

public class DataType {
  public static final int STRING = 1;
  public static final int BOOLEAN = 2;
  ...
}

However, if you use a String for the annotation parameter, we don't have a type checking system which extends into the type to specify that only particular values are allowed. In other words, Java lacks the ability to do something like this:

public int<values=[1,3,5,7..9]> oddInt; 

which would throw an error if you attempted to assign

 oddInt = 4;

Why is this important? Because if it doesn't apply to regular Java, then it cannot apply to the enumeration which is implemented in regular Java classes.

Solution 4

Little bit of improvisation with Java 8 Stream API

import static java.util.stream.Collectors.toList;
import static java.util.stream.Stream.of;
import java.util.List;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class EnumValidatorImpl implements ConstraintValidator<EnumValidator, String> 
{
  private List<String> valueList = null;
  @Override
  public boolean isValid(String value, ConstraintValidatorContext context) {
    return valueList.contains(value.toUpperCase());
  }
  @Override
  public void initialize(EnumValidator constraintAnnotation) {
    valueList = of(constraintAnnotation.enumClazz().getEnumConstants()).map(e->e.toString()).collect(toList());
  }
}

Solution 5

My attempt for a kotlin one:

import javax.validation.Constraint
import javax.validation.ConstraintValidator
import javax.validation.ConstraintValidatorContext
import javax.validation.ReportAsSingleViolation
import javax.validation.constraints.NotNull
import kotlin.reflect.KClass

@Constraint(validatedBy = [EnumValidatorImpl::class])
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FIELD)
@NotNull(message = "Value cannot be null")
@ReportAsSingleViolation
annotation class EnumValidator(val enumClazz: KClass<*>, val message: String = "Value is not valid")

class EnumValidatorImpl(private var valueList: List<String>? = null) : ConstraintValidator<EnumValidator, String> {
    override fun isValid(value: String?, context: ConstraintValidatorContext?): Boolean =
            valueList?.contains(value?.toUpperCase()) ?: false

    override fun initialize(constraintAnnotation: EnumValidator) {
        valueList = constraintAnnotation.enumClazz.java.enumConstants.map { it.toString().toUpperCase() }
    }
}
Share:
101,286
Joe
Author by

Joe

Updated on July 09, 2022

Comments

  • Joe
    Joe almost 2 years

    I want to validate a string against a set of values using annotations.

    What I want is basically this:

    @ValidateString(enumClass=com.co.enum)
    String dataType;
    
    int maxValue;
    int minValue;
    int precision;
    

    or

    @ValidateString(values={"String","Boolean", "Integer"})
    String dataType;
    
    int maxValue;
    int minValue;
    int precision;
    


    I also want to do some validation on other variables depending upon the value set in dataType:

    if (dataType = "String") {
        // maxValue, minValue, precision all should be null or zero
    }
    


    I can't think of a way to achieve this by custom annotations.
    Somebody please help me.

    • Bohemian
      Bohemian about 13 years
      What's the actual "business" need? What is this trying to solve? (I'm sure you don't actually need to do this exact thing - this is just a way to do something)
  • Thomas Stubbe
    Thomas Stubbe over 6 years
    "Ditch the String representation, and do a real enum." This is really the worst advice for user input validation... With a http-request for example, the user always sends a string. If the value is incorrect, the user expects to get a correct message (e.g. dataType does not match any known value. Use one of...) and not 400 BAD REQUEST, which would occur with a real enum
  • Thomas Stubbe
    Thomas Stubbe over 6 years
    The biggest disadvantage is that if the enum changes, this annotation is quickly forgotten. Isn't there a way to map this to the values of the enum?
  • Edwin Buck
    Edwin Buck over 6 years
    A simple static method on the enum valueForString(...) would return the Enum value, or null if there was none (due to a typo, for example). It centralizes validation of the input, and does so in the place where the inputs are defined. And let's not get over-dramatic. This isn't the worst advice, as we can both dream up much worse advice.
  • Sofiane
    Sofiane over 6 years
    Hi where is groups initialized ?
  • Shane Rowatt
    Shane Rowatt over 5 years
    @Sofiane the groups value it set when using the annotation as per the example above but is not used by the EnumValidatorImpl but instead is used by framework to managing validation groups.
  • Shane Rowatt
    Shane Rowatt over 5 years
    if you change those Enum[] to Enum<?>[] it gets rid of those unchecked cast warnings. Also the variable initialisation can be simplified to private final List<String> valueList = new ArrayList<>();
  • doga
    doga almost 5 years
    Does this code works for all enums and we need to only give enum class name in @EnumValidaror. Is this correct ?
  • postace
    postace almost 5 years
    the isValid() method can be simplified as: return valueList.contains(value.toUpperCase());