Files
spring-boot-demo/openapi-versioning/docs/02-what-springdoc-actually-generates.md
T
asmhatreandClaude Sonnet 5 8d0efb0d4b Add openapi-versioning: springdoc-openapi 3.1.1 vs Spring Framework 7 API versioning
Companion module for "springdoc-openapi with Spring Boot 4.1: Generating,
Customising and Versioning Your API Spec". Reuses the ApiVersionConfigurer
setup from the versioning-mechanics companion project and tests what
springdoc-openapi 3.1.1 actually generates for a path with multiple
version-scoped handlers: a default oneOf-merged operation with an
arbitrary operationId, a working GroupedOpenApi + OpenApiCustomizer fix
that collapses it to one clean schema per version, and a check of the
officially-versioning-supported functional-endpoint path (springdoc
v3.0.2's "Add support for Spring Framework API Versioning with Functional
Endpoints"), which turns out to document only one of two registered
versions rather than either version separately.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01M1uZDXWkY1MnTfQgt4HMeQ
2026-09-17 19:31:42 +00:00

75 lines
3.5 KiB
Markdown

# What springdoc actually generates for a versioned path
Full transcript: [docs/output/02-what-springdoc-actually-generates.txt](output/02-what-springdoc-actually-generates.txt),
produced by `OpenApiVersioningTest#b_defaultApiDocsMergesAllVersionsIntoOneOf`.
Hit the plain, ungrouped `/v3/api-docs` on this module — no custom configuration beyond adding
the starter — and here is the entire operation springdoc built for `GET /accounts/{id}`, which
has three handlers mapped to it (`version = "1"`, `"1.1+"`, `"2"`):
```json
{
"operationId": "getAccountV2",
"parameters": [
{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
{
"name": "API-Version",
"in": "header",
"schema": {
"type": "string",
"default": "1.0.0",
"enum": ["2", "1", "1.0.0", "1.1"]
}
}
],
"responses": {
"200": {
"content": {
"*/*": {
"schema": {
"oneOf": [
{ "$ref": "#/components/schemas/AccountV1" },
{ "$ref": "#/components/schemas/AccountV2" },
{ "$ref": "#/components/schemas/AccountV1Point1" }
]
}
}
}
}
}
}
```
Three things worth naming precisely, since none of them are documented behaviour as far as I
could find — this is what running it produced, not what any springdoc doc page promises:
**One operation, not three.** springdoc does not know that `version` is a routing discriminant
the way it knows `@PathVariable` or `@RequestParam` are. As far as its operation-building code is
concerned, three `HandlerMethod`s map to the same path and HTTP method, so it folds them into one
`Operation` — the same folding it would do for, say, an overloaded controller method disambiguated
by a `headers = "X-Foo"` condition.
**The response schema becomes `oneOf` all three shapes, with no discriminator.** This is
technically *not wrong* — a client that doesn't send `API-Version` really could get any of the
three shapes back, so `oneOf` is a defensible schema for "the operation's response, considered
without reference to versioning." But it is unusable as version-specific documentation: nothing in
this schema says which shape maps to which version.
**`operationId` is picked from whichever handler springdoc processes last, arbitrarily from the
reader's perspective.** It happened to be `getAccountV2` here. Generated API clients
(openapi-generator, openapi-typescript, etc.) use `operationId` as the method name they generate —
so a client generated from this spec gets one method, named after one version, that claims to
return `oneOf` three different shapes.
**The auto-documented `API-Version` header enum is also slightly wrong on its own terms**: it
lists `"1"` and `"1.0.0"` as if they were two different values, when `"1.0.0"` is just the
semantic-version-normalised form of the same default `"1"` configured in `WebConfig`
(`setDefaultVersion("1")`). Framework 7's version parser is doing exactly what it's supposed to —
`1` and `1.0.0` really do compare equal — but springdoc surfaces both the raw configured string and
the normalised form as if a caller could sensibly choose between them.
None of this is a crash — the document is valid OpenAPI 3.1, it just isn't the document most teams
actually want from a versioned API. [The next chapter](05-the-grouped-openapi-fix.md) is a working
fix for the first two problems; the header-enum oddity is cosmetic enough that I didn't chase it
further, and is called out in [06-known-issues.md](06-known-issues.md) instead.