Java String validation using enum values and annotation

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.


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;

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?


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.


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());
  }
}

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() }
    }
}