Search code examples
javakotlinbean-validationhibernate-validatorjavax-validation

Hibernate Validator: validate collection items before validating the collection itself


Problem:

I have a bean under validation by the Hibernate Validator.

That bean has a property with List type, and this property has custom validator (1). Elements of this List also have their own custom validators (2).

I need the elements of this list to be validated first, and only then, when validity of all list elements has been ensured, the whole list must be validated.

The difficulty is also in that the list is polymorphic and contains elements of different types, all are descendants of the one base type, and each descendant has their own custom validator.

Code example below (simplified):

// Validation root
class Bean(
    // List validator (1)
    @field:ValidList
    val list: List<BaseType>
)

// Base type for list elements (has no validator)
interface BaseType

// Descendant 1 of the BaseType with custom validator (2.1)
@ValidA
class A : BaseType

// Descendant 2 of the BaseType with custom validator (2.2)
@ValidB
class B : BaseType

By the code example above, I need the following validation order:

  1. @ValidA
  2. @ValidB
  3. @field:ValidList

Explanation of my needs:

The low-level validators on classes A and B must ensure the isolated validity of the objects A and B, whereas top-level list validator must validate elements of the list against each other.

By the time to validate the elements of the list relative to each other, each separate element must already be valid. Otherwise, the list validation, being performed first, will not make sense and may produce weird senseless violations if individual elements of the list are not valid.


Is there possibility to reach such behavior with Hibernate Validator?


I tried to solve my problem using @GroupSequence, but I can't determine how to use it correctly in my case.

Also I discovered that when I use annotation @Valid above the List field, Hibernate Validator always validates this List first, and only then validates inner objects, but if I don't use @Valid and use some annotation right inside the collection's generic like List<@ValidSome BaseType>, Hibernate Validator, in countrary, validates inner objects first (exactly what I want to achieve), but in this case it ignores the hierarchy and doesn't validate A and B by their annotations at all.


Solution

  • Okay, it seems like I've solved the problem. Indeed validation groups do the job, and fortunately even without using @GroupSequence.

    I have created a number of validation group interfaces and defined a desired order of them with which Hibernate Validator must validate my bean. Then I have placed my groups in annotations with the corresponding order of applying. And after that all that remains to do is to loop through the ordered group list and invoke Hibernate Validator with each group separately (checking whether validation with the next group has failed and breaking the loop if so).

    Groups definition and their usage in the validation annotations:

    object ValidationGroups {
        interface BeanClass
        interface BeanCollection
    
        val allOrdered = listOf(
            // 1. default group placed first to validate bean by annotations
            //    that are defined without groups at all, such as simple 
            //    @Positive or @NotEmpty on some plain property
            javax.validation.groups.Default::class,
    
            // 2. then the bean class level validators must be invoked (only if
            //    all the properties of this bean validated by Default group are valid)
            BeanClass::class,
    
            // 3. after all, if the bean is valid by their class level validator,
            //    invoking validators on the outer collection property level
            BeanCollection::class,
        )
    }
    
    class Bean(
        @field:ValidList(groups = [ValidationGroups.BeanCollection::class]) // 3. validated last
        val list: List<BaseType>
    )
    
    interface BaseType
    
    @ValidA(groups = [ValidationGroups.BeanClass::class]) // 2. validated second
    class A : BaseType {
        @field:Positive  // 1. validated first
        val id: Int
    }
    
    @ValidB(groups = [ValidationGroups.BeanClass::class]) // 2. validated second
    class B : BaseType {
        @field:NotEmpty // 1. validated first
        val name: String
    }
    
    

    And the Hibernate Validator invocation:

    for (group in ValidationGroups.allOrdered) {
        val violations = validator.validate(bean, group.java)
        if (violations.isNotEmpty()) {
            return violations
        }
    }