To start with, here are the relevant dependencies in my build.gradle:
implementation("org.springframework.boot:spring-boot-starter-web:2.7.18")
implementation("org.springdoc:springdoc-openapi-ui:1.8.0") {
exclude(group = "org.slf4j", module = "slf4j-api")
}
implementation("org.springdoc:springdoc-openapi-webmvc-core:1.8.0") {
exclude(group = "org.slf4j", module = "slf4j-api")
}
(slf4j-api is excluded from Springdoc because it conflicts with the slf4j version used by spring-boot-starter-web, leading to no logging output)
What I'm trying to achieve:
a POST endpoint to create a Foo resource. CreateFooRequest
is an abstract superclass, with two subclasses, CreateFooARequest
and CreateFooBRequest
. CreateFooRequest
is annotated with Jackson's @JsonSubTypes
, referencing the two subclasses.
This works just fine: in the OpenAPI specification generated from the Spring REST controller, I can see all these types as components, and my CreateFooController
correctly indicates that the OpenAPI operation accepts oneOf: [FooA, FooB]
.
However, on any CreateFooRequest
there are two fields which are themselves polymorphic.
The first field, config
, is of type Config
, with subtypes None
, One
and Many
.
The second field is a List<Animal>
, the subtypes of Animal
currently include Cat
and Dog
.
The request class therefore looks something like this:
@Schema
private Config config = new None();
@Schema
private List<Animal> animals = new ArrayList<>();
and both of Config
and Animal
look something like:
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(name = "none", value = None.class),
@JsonSubTypes.Type(name = "many", value = Many.class),
@JsonSubTypes.Type(name = "one", value = One.class),
})
@Schema(
subTypes = {
None.class,
Many.class,
One.class,
},
discriminatorProperty = "type",
discriminatorMapping = {
@DiscriminatorMapping(value = "none", schema = None.class),
@DiscriminatorMapping(value = "many", schema = Many.class),
@DiscriminatorMapping(value = "one", schema = One.class),
})
public interface Config {
}
Springdoc is able to detect the JSON schema types of these fields. For instance, createFooRequest.animals
is correctly described as
animals:
type: array
description: Some animals in the foo
items:
oneOf:
- $ref: '#/components/schemas/Cat'
- $ref: '#/components/schemas/Dog'
However, those schemas (#/components/schemas/Cat
and #/components/schemas/Dog
, Animal, Config and all its subtypes) are simply missing from the OpenAPI spec, making it an invalid spec. Annotating those types with @Schema
doesn't help.
Fields whose types are specific to one of those types are also excluded: for instance, if Dog is the only place where an enum Wibble {...
is used, then Wibble
will also be missing from the spec.
What do I need to do to get these schemas included? I would prefer not to have to explicitly include them myself using a GlobalOpenApiCustomizer
. This is because there could foreseeably be future subtypes of Animal
and I want them to be automatically included, rather than requiring that all future Animal
subtype developers must remember to add the new types if they want the spec to stay valid.
There seems to be a few issues working together against me, here. This is how I declare the request mapping in the @RestController
:
@PostMapping
public <T extends CreateFooRequest> ResponseEntity<Void> createFoo(
@Valid @NotNull @RequestBody T request) {
There's a step in OpenAPI generation, io.swagger.v3.core.filter.SpecFilter#removeBrokenReferenceDefinitions
. This is the culprit, although I can't work out why it's marking the Animal and Config types as broken references.
The controller declaration seems to mark only FooA
and FooB
as 'directly in use', but the logic in that method decides that the base Foo
request is also transitively used. In the generated spec, I can see CreateFooRequest
, CreateFooARequest
and CreateFooBRequest
, but I don't see any of the other types.
Animal
, Config
and their associated subtypes are all declared as fields on the base CreateFooRequest
. So it looks like it isn't recursively marking the schemas used by 'indirectly-used schemas', of which CreateFooRequest
is one, as themselves being in use, and therefore not a broken reference.
If I move the Config
field declaration to one of FooA
or FooB
, then I can see it and its implementations in the spec. They will also appear if I add @Schema(implementation = CreateFooRequest.class)
to the request body parameter. So the fact that it's declared on the parent is what's breaking it.
The List<Animal>
case still doesn't show when moved to FooA
, suggesting that it's not 'hopping far enough' to find the usage of the Animal
schema.
I can 'fix' this by adding the following property to my Spring application.yml:
springdoc:
remove-broken-reference-definitions: false
which skips this behaviour. I would love a more 'robust' fix, that correctly displays the dependent schemas without requiring this flag, if anyone can provide one.
---------- EDIT ----------
By changing my dependencies to:
implementation("org.springdoc:springdoc-openapi-ui:1.8.0") {
exclude(group = "org.slf4j", module = "slf4j-api")
}
implementation("io.swagger.core.v3:swagger-core:2.2.30") {
exclude(group = "org.slf4j", module = "slf4j-api")
}
i.e. explicitly upgrading swagger-core to the latest version (which ought to be compatible given the semver version), I'm now able to product the expected output in the spec file.