Search code examples
javaspring-bootopenapispringdocspringdoc-openui

Dynamically generate enumeration at runtime in springdoc-openapi


I am writing a Java 17 + Spring Boot 3.x REST application that uses spring-doc to generate our OpenAPI definition and a Java API client.

In this simplified example below, I would like to generate an API client and spec where the 'size' will be an enumeration where the set of finite values is resolved at runtime. Is there a hook within spring-doc that allows me to customize the below RelativeSize type?

// Example Spring controller method
@GetMapping()
Mono<RelativeSize> getRelativeSize() { ... }

// Example data type
class RelativeSize {
    // Example: possible values SMALLER, SMALL, MEDIUM, BIG, BIG_BUT_SMALLER_THAN_LARGE
    private String size;
}

If I hard coded the line below, I will get the results that I want but is missing the runtime resolution of the possible values. These are calculated once on server start up and will never change during the lifetime of a build.

class RelativeSize {
    @Schema(allowableValues={"TEENY", "TINY", "WEE", "QUANTUM"})
    private String size;
}

In the above, the generated OpenAPI and Java client model will have a nested RelativeSize.SizeEnum which is what I'm looking for but can't figure out how to set the allowableValues programmatically.

Additionally I have hundreds of classes that need the same kind of behavior.

Help appreciated. Many thanks


Solution

  • You can use a PropertyCustomizer:

    public class MyCustomizer implements PropertyCustomizer {
        @Override
        public Schema customize(Schema property, AnnotatedType type) {
            List<String> allowedValues = retrieveAllowedValues(property, type);
            if (CollectionUtils.isNotEmpty(allowedValues)) {
                property.setEnum(allowedValues);
            }
            return property;
        }
    }
    

    The tricky part is how to retrieve the allowed values. You can use custom annotations (or a @Constraint) on the fields to retrieve dynamically, moving this responsibility to a suitable bean or validator:

    private List<String> retrieveAllowedValues(AnnotatedType type) {
        if (type.getCtxAnnotations() == null) {
            return null;
        }
        return Arrays.stream(type.getCtxAnnotations())
            .flatMap(annotation -> Arrays.stream(annotation.annotationType().getAnnotations()))
            .filter(superAnnotation -> superAnnotation instanceof Constraint)
            .findFirst()
            .map(superAnnotation -> retrieveAllowedValuesFromConstraint((Constraint) superAnnotation))
            .orElse(null);
    }
    
    private List<String> retrieveAllowedValuesFromConstraint(Constraint superAnnotation) {
        Class<? extends ConstraintValidator<?,?>>[] validatorArray = superAnnotation.validatedBy();
        if (validatorArray == null || validatorArray.length == 0) {
            return null;
        }
        Class<? extends ConstraintValidator<?,?>> validatorClass = validatorArray[0];
        MyValidator<?, ?> myValidator = MyValidator.class.isAssignableFrom(validatorClass) ?
            (MyValidator<?,?>) beanFactory.getBean(validatorClass) :
            null;
        return myValidator != null ? myValidator.retrieveAllowedValues() : null;
    }