slovelo
slovelo

Reputation: 23

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

Upvotes: 1

Views: 651

Answers (1)

dcolazin
dcolazin

Reputation: 1074

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;
}

Upvotes: 1

Related Questions