48

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
  • 16,038
  • 10
  • 74
  • 104
Joe
  • 960
  • 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

10 Answers10

50

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
  • 16,038
  • 10
  • 74
  • 104
Rajeev Singla
  • 788
  • 6
  • 7
  • 4
    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
  • is this thread safe? Suppose we are using EnumValidator for multiple different enumClazz, the valueList can be uncertain. – edmundpie Jan 26 '23 at 03:55
30

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
  • 483
  • 7
  • 20
Joe
  • 960
  • 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
14

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
  • 69,361
  • 7
  • 100
  • 138
  • 2
    "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
  • 6
    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
  • Note: The problem I face with enums is that they are parsed from Strings way before bean validation kicks in. And while bean validation bundles all violations into 1 list, if an enum field is not correct, Jackson would throw a parsing exception with a message far from user or even developer friendly. – iwat0qs Jan 14 '23 at 13:40
  • @iwat0qs Jackson has excellent unmarshalling into enums, but one does have to extend Jackson with a custom serializer for the enum. Complaining that the enum is invalid is not something I understand, because when using enum objects, validity is that the object exists, invalidity is that it does not, and once Jackson parses it to an object, it either succeeds there or fails. Saying that bean validation then fails the parsing assumes a getter and a setter (to make a bean) and if you have those, you aren't using object based enums (or probably even enums). – Edwin Buck Jan 14 '23 at 14:34
  • I didn't say that an enum is invalid, or that "bean validation fails parsing". Bean validation is something that happens after parsing by Jackson. What I said, is that when Jackson faces a String value that could not be parsed into the target enum value, it throws a super ugly parsing exception. Having a utility method that returns null on invalid values is also not a universal solution, since it may be the case that null is a valid value, and silently falling back to null is not good then. 1/2 – iwat0qs Jan 15 '23 at 15:16
  • Also, I didn't mean "enum = bean". I meant that enums are used as fields inside beans/DTOs. First the data has to be parsed by Jackson, and when Jackson cannot parse a String into an enum, it will throw an exception, and bean validation for the container-DTO will not execute. Can you point me to an example of Jackson unmarshallers that can preserve the value or the error state up until the bean validation stage so that the invalid enum value error is bundled together with all other errors? Else I didn't get your point. 2/2 – iwat0qs Jan 15 '23 at 15:18
  • @iwat0qs I can't imagine a scenario where one would want to continue to parse additional fields after an error was raised. It seems you still say bean validation fails after Jackson parsing fails. Well, it's supposed to. I'm thinking you are doing something odd in your validation, and "fixing" invalid values into valid ones as you go. That's ignoring a lot of good practices, and while you may have to do so because of the legacy of your application, it's not like tools are SUPPOSED to help you do invalid things (like converting bad input into valid input). – Edwin Buck Jan 15 '23 at 18:11
  • I'm not doing anything odd in validation. The reason why I want to use bean validation, is because I want a single 400 response with ALL client mistakes listed, including the invalid String in place of an enum. From JSON perspective there are no enums. An invalid String in place of an enum is no different than a negative integer, where a positive one is expected. Jackson doesn't fail parsing. Jackson fails mapping, because bean validation runs too late. Something like JSON schema instead of bean validation would catch this before Jackson mapping from JSON to DTO. 1/2 – iwat0qs Jan 16 '23 at 22:18
  • Or Strings in DTO fields, so that Jackson doesn't fail mapping, then Bean Validation to validate the "enum" fields, and something like MapStruct to handle the mapping to enum boilerplate. Again, I'm not "fixing" any values. That's exactly what I said previously I want to avoid. I've said I don't want a silent null, when the user gave an invalid value. But I want the invalid enum to be reported together with all the invalid integers and blank strings and other validation errors, not as a separate exception cancelling bean validation 2/2 – iwat0qs Jan 16 '23 at 22:18
2

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,743
  • 3
  • 16
  • 28
2

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
  • 2,085
  • 3
  • 15
  • 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
  • 82
  • 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
  • 871
  • 8
  • 12
0

I know I'm a little late to the party (11 years late to be exact), but I'd still like to contribute.

The answers presented here are great, they solve the problem in most cases, but in my opinion they lack the personalization touch. What do I mean?

All solutions create the ConstraintValidator<EnumValidator, String> and implement the validation logic in it.

And that is great, it solve the problem, but, what happens if I want to make the comparison by the toString() of the enum, or better, I have another one that I want to compare by the name, two different comparisons. For this, it would be necessary to implement a ConstraintValidator for each type of comparison that is needed, when in fact their logic is VERY similar.

In my particular case, a very old system had comparisons, some with toUpperCase, others with toLoweCase, others with trim, some with name, others with toString, total chaos, and the idea was to generalize all this in the same behavior.

The solution that I present to you combines the excellent answer of @Rajeev with the necessary customization to be able to reuse the ConstraintValidator and implement only one comparison method for each different enum.

The general idea: Make the emun implement an interface to standardize the comparison.

First of all, the @annotation, nothing fancy:


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;

@Target(value = ElementType.FIELD)
@Retention(value = RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {EnumValidatorRegister_String.class})
public @interface EnumValidator {

    String message() default "Value is not present in enum list.";

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

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

    public String detailMessage() default "";

    public Class<? extends Enum<?>> target();
}

Keep in mind that the enum class will be presented in the target field.

Now the interface to generalize the behavior:

public interface EnumValidatorComparator<T> {

    public boolean test(T other);
}

A general combination of these two elements results in an enum with the generalized comparison behavior inside it, which in case it changes would only affect said implementation and no other elements of the system.

public enum Type implements EnumValidatorComparator<String> {
    a("a"),
    b("b"),
    c("c"),
    d("d"),
    e("e"),
    f("f"),
    g("g"),
    h("h"),
    i("i");

    private final String name;

    private Type(String name) {
        this.name = name;
    }

    @Override
    public boolean test(String other) {
        return this.toString().equalsIgnoreCase(other.trim().toLowerCase());
    }

}

Finally the ConstraintValidator, this is where the 'magic' happens.


import java.util.function.BiPredicate;
import java.util.stream.Stream;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;

@Slf4j
public class EnumValidatorRegister_String implements ConstraintValidator<EnumValidator, String> {

    //general comparator in case EnumValidator don't implement EnumValidatorComparator interface
    private static BiPredicate<? super Enum, String> defaultComparison = (currentEnumValue, testValue) -> {
        return currentEnumValue.toString().equals(testValue);
    };

    //setter for default comparator
    public static void setDefaultEnumComparator(BiPredicate<? super Enum, String> defaultComparison) {
        Assert.notNull(defaultComparison, "Default comparison can't be null");
        EnumValidatorRegister_String.defaultComparison = defaultComparison;
    }

    //Enum class
    private Class<? extends Enum<?>> clazz;
    //values of enum
    private Enum[] valuesArr;

    @Override
    public void initialize(EnumValidator _enum) {
        ConstraintValidator.super.initialize(_enum);
        clazz = _enum.target();
        valuesArr = clazz.getEnumConstants();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        boolean present;

        //if enum's targes is assignable from EnumValidatorComparator, compare by the `.test`
        if (EnumValidatorComparator.class.isAssignableFrom(clazz)) {
            present = Stream.of(valuesArr).anyMatch((t) -> {
                return ((EnumValidatorComparator<String>) t).test(value);
            });
        } else { //if enum's targes is NOT assignable from EnumValidatorComparator, compare by the default
            present = Stream.of(valuesArr).anyMatch((t) -> {
                return defaultComparison.test(t, value);
            });
        }

        //if the value is NOT present, show custom error
        if (!present) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(
                    String.format(
                            "'%s' is not one of the one of allowable values: %s".formatted(
                                    value,
                                    Stream.of(valuesArr).map((Object object) -> {
                                        return object.toString();
                                    }).toList().toString()
                            )
                    )
            ).addConstraintViolation();
        }

        return present;
    }
}

Note: lombok dependencies are used for @Slf4j for easy logging and springframework's Assert for validations of null value.

its use is as simple as expected:

    public class People {

        @EnumValidator(target = Type.class)
        private String name;
        
        private String someOtherField;
        
        //getters, setters and constructor
    }

In this way, if you have another enum with another comparison logic, it is as simple as creating said enum with its embedded logic, like:

public enum OtherType implements EnumValidatorComparator<String> {
    A("A"),
    B("B"),
    C("C"),
    D("D"),
    E("E");

    private final String name;

    private OtherType(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return name;
    }

    @Override
    public boolean test(String other) {
        return this.toString().equals(other);
    }

}

And this way the ConstraintValidator it's reused along all application, and changes only affect the class responsible for them without breaking the rest of the system logic.

Believe it or not this solution guaranteed me a raise at work, I hope I do something similar for you. :+1:

0

I would like to add my two cents too.

In the proposed solutions the enum type member is replaced with a string to enable using (javax|jakarta).validation for validating it.

I struggled with the same contemplation and had rather not change my enum attributes to strings, instead intercept the InvalidFormatException thrown by Jackson.

I'm using Spring boot rest controllers, and so the exception is caught by a controller advice.

The solution is written in Kotlin.

override fun handleHttpMessageNotReadable(
    ex: HttpMessageNotReadableException,
    headers: HttpHeaders,
    status: HttpStatusCode,
    request: WebRequest
): ResponseEntity<Any>? {
    return if (ex.cause is InvalidFormatException && (ex.cause as InvalidFormatException).targetType.isEnum) {
        val cause = ex.cause as InvalidFormatException
        val invalidValue = cause.value
        val enumClass = cause.targetType
        val fieldPath = prettyJsonPath(cause.path)
        val entries = enumClass.enumConstants.joinToString(", ")
        val apiError = ApiError(
            status = HttpStatus.BAD_REQUEST,
            message = "Request is bad, please check provided errors.",
            errors = listOf("Invalid value ($invalidValue) for $fieldPath, expected a value in [$entries].")
        )
        handleExceptionInternal(ex, apiError, headers, apiError.status, request)
    } else {
        super.handleHttpMessageNotReadable(ex, headers, status, request)
    }
}

private fun prettyJsonPath(path: List<JsonMappingException.Reference>): String {
    return path.mapIndexed { index, ref ->
        val dot = if (index == 0 || ref.index >= 0) "" else "."
        val part = if (ref.index == -1) ref.fieldName else "[${ref.index}]"
        return@mapIndexed "$dot$part"
    }.joinToString("")
}
Ittai
  • 61
  • 10
-4

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
  • 30,615
  • 24
  • 120
  • 162
Kalyan
  • 1