javaspringspring-bootspring-data-restbean-validation

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

  • A Hibernate Validation DefaultGroupSequenceProvider can be used to dynamically define the default validation group based on the state of the object.

    Per the reference guide:

    Hibernate Validator also provides an SPI for the dynamic redefinition of default group sequences depending on the object state.

    For that purpose, you need to implement the interface DefaultGroupSequenceProvider and register this implementation with the target class via the @GroupSequenceProvider annotation.

    In this case, a DefaultGroupSequenceProvider can be created which uses the Flying group (plus the standard default group) when the object's type property is FLYING, and the standard default group otherwise.

    import org.hibernate.validator.spi.group.DefaultGroupSequenceProvider;
    import java.util.List;
    
    public class AnimalTypeGroupSequenceProvider
            implements DefaultGroupSequenceProvider<Animal> {
        @Override
        public List<Class<?>> getValidationGroups(Animal object) {
            if (object != null && object.getType() == Animal.Type.FLYING) {
                return List.of(Animal.Flying.class, Animal.class);
            } else {
                return List.of(Animal.class);
            }
        }
    }
    
    import org.hibernate.validator.group.GroupSequenceProvider;
    import javax.persistence.Entity;
    
    @GroupSequenceProvider(AnimalTypeGroupSequenceProvider.class)
    @Entity
    public class Animal {
       // ...
    }