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
This commit is contained in:
2026-09-17 19:31:42 +00:00
co-authored by Claude Sonnet 5
parent a875bea55a
commit 8d0efb0d4b
26 changed files with 1163 additions and 0 deletions
@@ -0,0 +1,60 @@
# The fix: one GroupedOpenApi per version
Full transcript: [docs/output/05-grouped-openapi-fix.txt](output/05-grouped-openapi-fix.txt), from
`OpenApiVersioningTest#c_groupedOpenApiCustomizerCollapsesToOneCleanSchemaPerVersion`.
[`VersionedOpenApiConfig`](../src/main/java/com/ankurm/openapiversioning/config/VersionedOpenApiConfig.java)
registers three `GroupedOpenApi` beans — one per supported version — each scoped to
`/accounts/**` and each carrying an `OpenApiCustomizer` that collapses the merged `oneOf` response
from the previous chapter down to the one schema that version's clients actually receive, fixes
`operationId` and `summary` to match, and drops the now-meaningless `API-Version` header:
```java
@Bean
public GroupedOpenApi accountsV1Group() {
return groupFor("accounts-v1", "1");
}
private GroupedOpenApi groupFor(String groupName, String version) {
VersionSpec spec = SPECS.get(version);
return GroupedOpenApi.builder()
.group(groupName)
.pathsToMatch("/accounts/**")
.addOpenApiCustomizer(collapseToSingleVersion(spec))
.build();
}
```
Each group is served at `/v3/api-docs/{group}` (springdoc's own convention — no extra wiring
needed). Result, all three fetched from the same running instance:
```
GET /v3/api-docs/accounts-v1 -> operationId=getAccountV1, schema=AccountV1, params=[id]
GET /v3/api-docs/accounts-v1.1 -> operationId=getAccountV1Point1Plus, schema=AccountV1Point1, params=[id]
GET /v3/api-docs/accounts-v2 -> operationId=getAccountV2, schema=AccountV2, params=[id]
```
Each is a clean, single-schema operation — feed any one of these three documents into
openapi-generator and you get one method whose declared response type is actually what that
version's clients receive, not a `oneOf` of three.
**The default, ungrouped `/v3/api-docs` is untouched** — confirmed by re-fetching it inside the
same test, immediately after fetching all three groups, and it still shows the exact `oneOf` merge
from the previous chapter. `GroupedOpenApi`'s customizer runs against a document scoped to that
group's own build, not the shared default document, so adding version-specific groups is additive:
existing tooling pointed at the plain `/v3/api-docs` (or Swagger UI's default view) keeps working
exactly as it did before, while `/v3/api-docs/accounts-v2` becomes a new, additional,
clean-per-version resource for anyone who wants one — a generated TypeScript client for your v2
mobile app, say, without also generating types for the v1 shape it will never receive.
See [the previous chapter](03-why-a-hand-maintained-map.md) for why the version→schema mapping
this customizer applies is a small hand-written `Map`, not something derived automatically from
the running handler mappings — that's a real limitation of the public springdoc customizer API,
not a shortcut taken for the sake of the demo.
## Swagger UI
With three additional groups registered, `/swagger-ui.html` grows a group selector (`default`,
`accounts-v1`, `accounts-v1.1`, `accounts-v2`) — switching between them re-fetches the
corresponding `/v3/api-docs/{group}` document, so the same collapsed, single-schema view is what a
human reading the UI sees too, not just API clients hitting the JSON directly.