Hibernate Validation of Collections of Primitives Hibernate Validation of Collections of Primitives java java

Hibernate Validation of Collections of Primitives


Neither JSR-303 nor Hibernate Validator has any ready-made constraint that can validate each elements of Collection.

One possible solution to address this issue is to create a custom @ValidCollection constraint and corresponding validator implementation ValidCollectionValidator.

To validate each element of collection we need an instance of Validator inside ValidCollectionValidator; and to get such instance we need custom implementation of ConstraintValidatorFactory.

See if you like following solution...

Simply,

  • copy-paste all these java classes (and import relavent classes);
  • add validation-api, hibenate-validator, slf4j-log4j12, and testng jars on classpath;
  • run the test-case.

ValidCollection

    public @interface ValidCollection {    Class<?> elementType();    /* Specify constraints when collection element type is NOT constrained      * validator.getConstraintsForClass(elementType).isBeanConstrained(); */    Class<?>[] constraints() default {};    boolean allViolationMessages() default true;    String message() default "{ValidCollection.message}";    Class<?>[] groups() default {};    Class<? extends Payload>[] payload() default {};}

ValidCollectionValidator

    public class ValidCollectionValidator implements ConstraintValidator<ValidCollection, Collection>, ValidatorContextAwareConstraintValidator {    private static final Logger logger = LoggerFactory.getLogger(ValidCollectionValidator.class);    private ValidatorContext validatorContext;    private Class<?> elementType;    private Class<?>[] constraints;    private boolean allViolationMessages;    @Override    public void setValidatorContext(ValidatorContext validatorContext) {        this.validatorContext = validatorContext;    }    @Override    public void initialize(ValidCollection constraintAnnotation) {        elementType = constraintAnnotation.elementType();        constraints = constraintAnnotation.constraints();        allViolationMessages = constraintAnnotation.allViolationMessages();    }    @Override    public boolean isValid(Collection collection, ConstraintValidatorContext context) {        boolean valid = true;        if(collection == null) {            //null collection cannot be validated            return false;        }        Validator validator = validatorContext.getValidator();        boolean beanConstrained = validator.getConstraintsForClass(elementType).isBeanConstrained();        for(Object element : collection) {            Set<ConstraintViolation<?>> violations = new HashSet<ConstraintViolation<?>> ();            if(beanConstrained) {                boolean hasValidCollectionConstraint = hasValidCollectionConstraint(elementType);                if(hasValidCollectionConstraint) {                    // elementType has @ValidCollection constraint                    violations.addAll(validator.validate(element));                } else {                    violations.addAll(validator.validate(element));                }            } else {                for(Class<?> constraint : constraints) {                    String propertyName = constraint.getSimpleName();                    propertyName = Introspector.decapitalize(propertyName);                    violations.addAll(validator.validateValue(CollectionElementBean.class, propertyName, element));                }            }            if(!violations.isEmpty()) {                valid = false;            }            if(allViolationMessages) { //TODO improve                for(ConstraintViolation<?> violation : violations) {                    logger.debug(violation.getMessage());                    ConstraintViolationBuilder violationBuilder = context.buildConstraintViolationWithTemplate(violation.getMessage());                    violationBuilder.addConstraintViolation();                }            }        }        return valid;    }    private boolean hasValidCollectionConstraint(Class<?> beanType) {        BeanDescriptor beanDescriptor = validatorContext.getValidator().getConstraintsForClass(beanType);        boolean isBeanConstrained = beanDescriptor.isBeanConstrained();        if(!isBeanConstrained) {            return false;        }        Set<ConstraintDescriptor<?>> constraintDescriptors = beanDescriptor.getConstraintDescriptors();         for(ConstraintDescriptor<?> constraintDescriptor : constraintDescriptors) {            if(constraintDescriptor.getAnnotation().annotationType().getName().equals(ValidCollection.class.getName())) {                return true;            }        }        Set<PropertyDescriptor> propertyDescriptors = beanDescriptor.getConstrainedProperties();        for(PropertyDescriptor propertyDescriptor : propertyDescriptors) {            constraintDescriptors = propertyDescriptor.getConstraintDescriptors();            for(ConstraintDescriptor<?> constraintDescriptor : constraintDescriptors) {                if(constraintDescriptor.getAnnotation().annotationType().getName().equals(ValidCollection.class.getName())) {                    return true;                }            }            }        return false;    }}

ValidatorContextAwareConstraintValidator

public interface ValidatorContextAwareConstraintValidator {    void setValidatorContext(ValidatorContext validatorContext);}

CollectionElementBean

    public class CollectionElementBean {    /* add more properties on-demand */    private Object notNull;    private String notBlank;    private String email;    protected CollectionElementBean() {    }    @NotNull    public Object getNotNull() { return notNull; }    public void setNotNull(Object notNull) { this.notNull = notNull; }    @NotBlank    public String getNotBlank() { return notBlank; }    public void setNotBlank(String notBlank) { this.notBlank = notBlank; }    @Email    public String getEmail() { return email; }    public void setEmail(String email) { this.email = email; }}

ConstraintValidatorFactoryImpl

public class ConstraintValidatorFactoryImpl implements ConstraintValidatorFactory {    private ValidatorContext validatorContext;    public ConstraintValidatorFactoryImpl(ValidatorContext nativeValidator) {        this.validatorContext = nativeValidator;    }    @Override    public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {        T instance = null;        try {            instance = key.newInstance();        } catch (Exception e) {             // could not instantiate class            e.printStackTrace();        }        if(ValidatorContextAwareConstraintValidator.class.isAssignableFrom(key)) {            ValidatorContextAwareConstraintValidator validator = (ValidatorContextAwareConstraintValidator) instance;            validator.setValidatorContext(validatorContext);        }        return instance;    }}

Employee

public class Employee {    private String firstName;    private String lastName;    private List<String> emailAddresses;    @NotNull    public String getFirstName() { return firstName; }    public void setFirstName(String firstName) { this.firstName = firstName; }    public String getLastName() { return lastName; }    public void setLastName(String lastName) { this.lastName = lastName; }    @ValidCollection(elementType=String.class, constraints={Email.class})    public List<String> getEmailAddresses() { return emailAddresses; }    public void setEmailAddresses(List<String> emailAddresses) { this.emailAddresses = emailAddresses; }}

Team

public class Team {    private String name;    private Set<Employee> members;    public String getName() { return name; }    public void setName(String name) { this.name = name; }    @ValidCollection(elementType=Employee.class)    public Set<Employee> getMembers() { return members; }    public void setMembers(Set<Employee> members) { this.members = members; }}

ShoppingCart

public class ShoppingCart {    private List<String> items;    @ValidCollection(elementType=String.class, constraints={NotBlank.class})    public List<String> getItems() { return items; }    public void setItems(List<String> items) { this.items = items; }}

ValidCollectionTest

public class ValidCollectionTest {    private static final Logger logger = LoggerFactory.getLogger(ValidCollectionTest.class);    private ValidatorFactory validatorFactory;    @BeforeClass    public void createValidatorFactory() {        validatorFactory = Validation.buildDefaultValidatorFactory();    }    private Validator getValidator() {        ValidatorContext validatorContext = validatorFactory.usingContext();        validatorContext.constraintValidatorFactory(new ConstraintValidatorFactoryImpl(validatorContext));        Validator validator = validatorContext.getValidator();        return validator;    }    @Test    public void beanConstrained() {        Employee se = new Employee();        se.setFirstName("Santiago");        se.setLastName("Ennis");        se.setEmailAddresses(new ArrayList<String> ());        se.getEmailAddresses().add("segmail.com");        Employee me = new Employee();        me.setEmailAddresses(new ArrayList<String> ());        me.getEmailAddresses().add("me@gmail.com");        Team team = new Team();        team.setMembers(new HashSet<Employee>());        team.getMembers().add(se);        team.getMembers().add(me);        Validator validator = getValidator();        Set<ConstraintViolation<Team>> violations = validator.validate(team);        for(ConstraintViolation<Team> violation : violations) {            logger.info(violation.getMessage());        }    }    @Test    public void beanNotConstrained() {        ShoppingCart cart = new ShoppingCart();        cart.setItems(new ArrayList<String> ());        cart.getItems().add("JSR-303 Book");        cart.getItems().add("");        Validator validator = getValidator();        Set<ConstraintViolation<ShoppingCart>> violations = validator.validate(cart, Default.class);        for(ConstraintViolation<ShoppingCart> violation : violations) {            logger.info(violation.getMessage());        }    }}

Output

02:16:37,581  INFO main validation.ValidCollectionTest:66 - {ValidCollection.message}02:16:38,303  INFO main validation.ValidCollectionTest:66 - may not be null02:16:39,092  INFO main validation.ValidCollectionTest:66 - not a well-formed email address02:17:46,460  INFO main validation.ValidCollectionTest:81 - may not be empty02:17:47,064  INFO main validation.ValidCollectionTest:81 - {ValidCollection.message}

Note:- When bean has constraints do NOT specify the constraints attribute of @ValidCollection constraint. The constraints attribute is necessary when bean has no constraint.


I don't have a high enough reputation to comment this on the original answer, but perhaps it is worth noting on this question that JSR-308 is in its final release stage and will address this problem when it is released! It will at least require Java 8, however.

The only difference would be that the validation annotation would go inside the type declaration.

//@Emailpublic List<@Email String> getEmailAddresses(){   return this.emailAddresses;}

Please let me know where you think I could best put this information for others who are looking. Thanks!

P.S. For more info, check out this SO post.


It’s not possible to write a generic wrapper annotation like @EachElement to wrap any constraint annotation — due to limitations of Java Annotations itself. However, you can write a generic constraint validator class which delegates actual validation of every element to an existing constraint validator. You have to write a wrapper annotation for every constraint, but just one validator.

I’ve implemented this approach in jirutka/validator-collection (available in Maven Central). For example:

@EachSize(min = 5, max = 255)List<String> values;

This library allows you to easily create a “pseudo constraint” for any validation constraint to annotate a collection of simple types, without writing an extra validator or unnecessary wrapper classes for every collection. EachX constraint is supported for all standard Bean Validation constraints and Hibernate specific constraints.

To create an @EachAwesome for your own @Awesome constraint, just copy&paste the annotation class, replace @Constraint annotation with @Constraint(validatedBy = CommonEachValidator.class) and add the annotation @EachConstraint(validateAs = Awesome.class). That’s all!

// common boilerplate@Documented@Retention(RUNTIME)@Target({METHOD, FIELD, ANNOTATION_TYPE})// this is important!@EachConstraint(validateAs = Awesome.class)@Constraint(validatedBy = CommonEachValidator.class)public @interface EachAwesome {    // copy&paste all attributes from Awesome annotation here    String message() default "";    Class<?>[] groups() default {};    Class<? extends Payload>[] payload() default {};    String someAttribute();}

EDIT: Updated for the current version of library.