42

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.

informatik01
  • 15,636
  • 10
  • 72
  • 102
Joe
  • 860
  • 1
  • 8
  • 11
  • 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) – Bohemian Jun 09 '11 at 14:43

8 Answers8

42

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;
informatik01
  • 15,636
  • 10
  • 72
  • 102
Rajeev Singla
  • 698
  • 6
  • 7
  • 3
    Hi where is groups initialized ? – Sofiane Dec 20 '17 at 13:40
  • @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 Jan 08 '19 at 05:55
  • Does this code works for all enums and we need to only give enum class name in @EnumValidaror. Is this correct ? – doga Aug 10 '19 at 11:17
  • the `isValid()` method can be simplified as: `return valueList.contains(value.toUpperCase());` – postace Aug 24 '19 at 13:52
26

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?

Tarek B
  • 443
  • 7
  • 20
Joe
  • 860
  • 1
  • 8
  • 11
  • 5
    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? – Thomas Stubbe Dec 15 '17 at 15:19
13

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.

Edwin Buck
  • 67,527
  • 7
  • 97
  • 130
  • 1
    "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 Dec 15 '17 at 15:17
  • 5
    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. – Edwin Buck Dec 15 '17 at 19:07
1

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());
  }
}
Mahesh_Loya
  • 2,552
  • 3
  • 14
  • 28
1

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() }
    }
}
Léo Schneider
  • 1,837
  • 3
  • 12
  • 28
0

I take up Rajeev Singla's response https://stackoverflow.com/a/21070806/8923905, just to optimize the code and allow the String parameter to be null, if in your application it is not mandatory and can be empty :

1- Remove the @NotNull annotation on the Interface

2- See the modified code below for the implementation.

public class EnumValidatorImpl implements ConstraintValidator <EnumValidator,String> {

    private List<String> valueList = null;

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

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

        Enum[] enumValArr = enumClass.getEnumConstants();

        for(Enum enumVal : enumValArr) {
            valueList.add(enumVal.toString().toUpperCase());
        }

    }
}
Emmanuel H
  • 72
  • 8
  • 1
    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 valueList = new ArrayList<>();` – Shane Rowatt Jan 08 '19 at 06:04
0

Here is a detailed example with the feature of a dynamic error message by Hibernate Documentation

https://docs.jboss.org/hibernate/validator/4.1/reference/en-US/html/validator-customconstraints.html#validator-customconstraints-simple

GC001
  • 829
  • 8
  • 12
-3

You can use @NotNull annotation in conjunction with yours. To use that you need to add @Target( { ANNOTATION_TYPE }) annotation in ValidateString.

http://docs.jboss.org/hibernate/validator/4.0.1/reference/en/html/validator-customconstraints.html

Mariusz Jamro
  • 29,116
  • 24
  • 107
  • 151
Kalyan
  • 1