Reputation: 23
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
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