Search code examples
javavalidationbean-validation

Java generic type validation


I have a generic super class which I'd like to validate:

abstract class Generic<T> {
    // ... other validated fields

    private T value;
}

Then I have several concrete classes and for each of it I'd like to appy specific validations on a value field.

E.g. for the below class I'd like to ensure that the value field is not less than 0.

class ConcreteInt<Integer> {
//    @Min(0)
}

Another example, for the below class I'd like to ensure that the value is not blank.

class ConcreteString<String> {
//    @NotBlank
}

Where should I put my validation annotations on a concrete classes to make them work?


Solution

  • I would suggest to use the Template design pattern and apply the validations accordingly.

    First we have the generic class that accepts any type as T with an abstract method that the children must implement.

    public abstract class Generic<T> {
        protected T value;
        abstract T getValue();
    }
    

    then we have a child for integer implementation with annotations @NotNull and @PositiveOrZero.

    import javax.validation.constraints.NotNull;
    import javax.validation.constraints.PositiveOrZero;
    
    public class ConcreteInteger extends Generic<Integer> {
        public ConcreteInteger(Integer v) {
            value = v;
        }
        @Override
        @NotNull
        @PositiveOrZero
        public Integer getValue() {
            return value;
        }
    }
    

    and a child for string implementation with annotations @NotNull, and @NotBlank. Here we already see that we can have different validations.

    package com.yieldlab.reporting.dto.analytics;
    
    import javax.validation.constraints.NotBlank;
    import javax.validation.constraints.NotNull;
    
    public class ConcreteString extends Generic<String> {
        public ConcreteString(String v) {
            value = v;
        }
        @Override
        @NotNull
        @NotBlank
        public String getValue() {
            return value;
        }
    }
    

    But it won't validate if we just use the annotations. We have to invoke the javax.validation.Validator API. Here is one example:

    import javax.validation.Configuration;
    import javax.validation.ConstraintViolation;
    import javax.validation.Validation;
    import javax.validation.Validator;
    import javax.validation.ValidatorFactory;
    
    public class BeanFieldValidationExample {
        private static final Validator validator;
        static {
            Configuration<?> config = Validation.byDefaultProvider().configure();
            ValidatorFactory factory = config.buildValidatorFactory();
            validator = factory.getValidator();
            factory.close();
        }
        public static void main(String[] args) {
            // it won't validate by just call the constructor
            ConcreteInteger concreteInteger0 = new ConcreteInteger(0);
            System.out.println(concreteInteger0.value);
            ConcreteInteger concreteInteger1 = new ConcreteInteger(1);
            System.out.println(concreteInteger1.value);
            ConcreteInteger concreteIntegerMinus1 = new ConcreteInteger(-1);
            System.out.println(concreteIntegerMinus1.value);
            ConcreteInteger concreteIntegerNull = new ConcreteInteger(null);
            System.out.println(concreteIntegerNull.value);
    
            ConcreteString concreteString0 = new ConcreteString("some string");
            System.out.println(concreteString0.value);
            ConcreteString concreteString1 = new ConcreteString("");
            System.out.println(concreteString1.value);
            ConcreteString concreteStringNull = new ConcreteString(null);
            System.out.println(concreteStringNull.value);
    
            // invoking the validator API will in validate our beans validator.validate(concreteInteger0).stream().forEach(BeanFieldValidationExample::printErrorConcreteInteger);
            validator.validate(concreteInteger1).stream().forEach(BeanFieldValidationExample::printErrorConcreteInteger);
            validator.validate(concreteIntegerMinus1).stream().forEach(BeanFieldValidationExample::printErrorConcreteInteger);
            validator.validate(concreteIntegerNull).stream().forEach(BeanFieldValidationExample::printErrorConcreteInteger);
    
            validator.validate(concreteString0).stream().forEach(BeanFieldValidationExample::printErrorConcreteString);
            validator.validate(concreteString1).stream().forEach(BeanFieldValidationExample::printErrorConcreteString);
            validator.validate(concreteStringNull).stream().forEach(BeanFieldValidationExample::printErrorConcreteString);
        }
    
        private static void printErrorConcreteInteger(ConstraintViolation<ConcreteInteger> concreteIntegerConstraintViolation) {
            System.out.println(concreteIntegerConstraintViolation.getPropertyPath() + " " + concreteIntegerConstraintViolation.getMessage());
        }
    
        private static void printErrorConcreteString(ConstraintViolation<ConcreteString> concreteStringConstraintViolation) {
            System.out.println(concreteStringConstraintViolation.getPropertyPath() + " " + concreteStringConstraintViolation.getMessage());
        }
    }
    

    then we can see the output:

    0
    1
    -1
    null
    some string
    
    null
    value must be greater than or equal to 0
    value must not be null
    value must not be blank
    value must not be null
    value must not be blank