'How can I conditionally apply validation groups in Spring Data REST?

I have a Spring Data REST project with an entity type with conditional validation based on a property of the entity. I want to enable certain validations using validation groups when that property is set to a specific value.

As a concrete example, take the following entity class:

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.validation.constraints.NotNull;

@Entity
public class Animal {
    public enum Type { FLYING, OTHER }

    /**
     * Validation group.
     */
    public interface Flying {}

    @Id
    @GeneratedValue
    private Integer id;

    private Type type;

    @NotNull(groups = Flying.class)
    private Integer airSpeedVelocity;

    @NotNull
    private Integer weight;

    public Integer getId() { return id; }
    public void setId(Integer id) { this.id = id; }
    public Type getType() { return type; }
    public void setType(Type type) { this.type = type; }
    public Integer getAirSpeedVelocity() { return airSpeedVelocity; }
    public void setAirSpeedVelocity(Integer airSpeedVelocity) { this.airSpeedVelocity = airSpeedVelocity; }
    public Integer getWeight() { return weight; }
    public void setWeight(Integer weight) { this.weight = weight;}
}

When saving an Animal with type FLYING, I want to validate that airSpeedVelocity is non-null. When saving any other animal, I don't want this validation.

Currently, I have validations enable to be checked prior to save, so that a 400 Bad Request error is returned if an object is invalid:

    @Bean
    public ValidatingRepositoryEventListener preSaveValidator(
            @Qualifier("defaultValidator") SmartValidator validator,
            ObjectFactory<PersistentEntities> persistentEntitiesFactory) {
        ValidatingRepositoryEventListener eventListener = 
                new ValidatingRepositoryEventListener(persistentEntitiesFactory);
        eventListener.addValidator("beforeCreate", validator);
        eventListener.addValidator("beforeSave", validator);
        return eventListener;
    }
}

Request:

{ "type": "FLYING" }

Current 400 error response:

{
    "errors": [
        {
            "entity": "Animal",
            "property": "weight",
            "invalidValue": null,
            "message": "must not be null"
        }
    ]
}

Desired 400 error response:

{
    "errors": [
        {
            "entity": "Animal",
            "property": "airSpeedVelocity",
            "invalidValue": null,
            "message": "must not be null"
        },
        {
            "entity": "Animal",
            "property": "weight",
            "invalidValue": null,
            "message": "must not be null"
        }
    ]
}

How can I perform this conditional validation, applying the Flying validation group when the request entity is an Animal where type == FLYING?



Solution 1:[1]

One solution is to use a customized Validator that automatically checks the input type, automatically applying the custom validation groups if necessary:

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.rest.core.event.ValidatingRepositoryEventListener;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.SmartValidator;

import javax.validation.groups.Default;

@Configuration
public class RestRepositoryValidatorConfig {
    @Bean
    public ValidatingRepositoryEventListener preSaveValidator(
            AnimalValidationGroupAwareValidator validator,
            ObjectFactory<PersistentEntities> persistentEntitiesFactory) {
        ValidatingRepositoryEventListener eventListener =
                new ValidatingRepositoryEventListener(persistentEntitiesFactory);
        eventListener.addValidator("beforeCreate", validator);
        eventListener.addValidator("beforeSave", validator);
        return eventListener;
    }

    @Component
    public static class AnimalValidationGroupAwareValidator
            implements SmartValidator {
        private final SmartValidator delegate;

        public AnimalValidationGroupAwareValidator(
                @Qualifier("defaultValidator") SmartValidator delegate) {
            this.delegate = delegate;
        }

        @Override
        public boolean supports(Class<?> clazz) {
            return true;
        }

        @Override
        public void validate(Object target, Errors errors,
                             Object... validationHints) {
            // If hints are overridden, use those instead
            delegate.validate(target, errors, validationHints);
        }

        @Override
        public void validate(Object target, Errors errors) {
            if (target instanceof Animal animal &&
                Animal.Type.FLYING.equals(animal.getType())) {
                delegate.validate(target, errors,
                        Animal.Flying.class, Default.class);
            } else {
                delegate.validate(target, errors);
            }
        }
    }
}

Note that this also adds the Default validation group, since otherwise the standard validations would not also be performed.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 M. Justin