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
100 lines
5.5 KiB
Markdown
100 lines
5.5 KiB
Markdown
# The functional-endpoint path: officially supported, and worse
|
||
|
||
springdoc-openapi's [v3.0.2 release notes](https://github.com/springdoc/springdoc-openapi/releases/tag/v3.0.2)
|
||
(28 Feb 2026) list, under **Added**: "#3229 – Add support for Spring Framework API Versioning with
|
||
Functional Endpoints." Reading that changelog line on its own, it's reasonable to expect functional
|
||
(`RouterFunction`) endpoints to be the *better*-supported path for versioned APIs in springdoc —
|
||
official, named support, versus the annotated-controller behaviour in the previous chapter that
|
||
nothing documents. I built [`WidgetRouterConfig`](../src/main/java/com/ankurm/openapiversioning/functional/WidgetRouterConfig.java)
|
||
to check.
|
||
|
||
Full transcript: [docs/output/06-functional-endpoint-path.txt](output/06-functional-endpoint-path.txt),
|
||
from `OpenApiVersioningTest#d_functionalEndpointVersioningRuntimeWorksButDocsDoNot`.
|
||
|
||
## The setup
|
||
|
||
Two handler methods, one `RouterFunction` with Framework 7's `RequestPredicates.version(...)`
|
||
doing the routing, and springdoc's `@RouterOperations`/`@RouterOperation` — the mechanism
|
||
functional endpoints have always needed for OpenAPI documentation, since there's no
|
||
`@GetMapping` for springdoc to reflect on:
|
||
|
||
```java
|
||
@Bean
|
||
@RouterOperations({
|
||
@RouterOperation(path = "/widgets/{id}", method = GET, beanClass = WidgetHandler.class, beanMethod = "getWidgetV1"),
|
||
@RouterOperation(path = "/widgets/{id}", method = GET, beanClass = WidgetHandler.class, beanMethod = "getWidgetV2")
|
||
})
|
||
public RouterFunction<ServerResponse> widgetRoutes(WidgetHandler handler) {
|
||
return RouterFunctions.route()
|
||
.GET("/widgets/{id}", RequestPredicates.version("1"), handler::getWidgetV1)
|
||
.GET("/widgets/{id}", RequestPredicates.version("2"), handler::getWidgetV2)
|
||
.build();
|
||
}
|
||
```
|
||
|
||
First thing worth checking before running anything: `@RouterOperation` itself has **no version
|
||
attribute** (`javap org/springdoc/core/annotations/RouterOperation.class` — `path`, `method`,
|
||
`consumes`, `produces`, `headers`, `params`, `beanClass`, `beanMethod`, `parameterTypes`,
|
||
`operation`; nothing else). Whatever PR #3229 added, it did not add a way to tell springdoc which
|
||
version a given `@RouterOperation` documents.
|
||
|
||
## Runtime: correct
|
||
|
||
```
|
||
GET /widgets/7, API-Version: 1 -> {"id":"7","apiVersion":"1"}
|
||
GET /widgets/7, API-Version: 2 -> {"widgetId":"7","apiVersion":"2"}
|
||
```
|
||
|
||
Framework 7's `version(...)` `RequestPredicate` routes correctly — no surprise there, that's
|
||
Framework's job, not springdoc's, and it was already verified independently in the companion
|
||
project for the versioning-mechanics article.
|
||
|
||
## Documentation: worse than the annotated-controller case
|
||
|
||
```json
|
||
{
|
||
"tags": ["widget-handler"],
|
||
"operationId": "getWidgetV1",
|
||
"responses": {
|
||
"200": {
|
||
"content": { "*/*": { "schema": { "$ref": "#/components/schemas/ServerResponse" } } }
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Two registered `@RouterOperation`s went in; **one operation came out**, silently — not merged into
|
||
a `oneOf` the way the annotated `@GetMapping` handlers were in the previous chapter, just dropped.
|
||
In this build it was consistently the first-declared entry (`getWidgetV1`) that survived across
|
||
repeated `mvn test` runs; I did not chase the exact selection rule (declaration order in the
|
||
`@RouterOperations` array vs. `RouterFunction` registration order) because it doesn't change the
|
||
practical takeaway — springdoc does not attempt to represent both versions of a functional
|
||
endpoint at all, which is a strictly worse outcome for a reader than the annotated-controller
|
||
`oneOf` merge, not a fix for it.
|
||
|
||
The response schema resolving to a near-empty `ServerResponse` component (rather than either
|
||
handler's actual JSON shape) is a separate, pre-existing springdoc limitation of functional
|
||
endpoints in general: `@RouterOperation.operation()` lets you attach a full `@Operation`
|
||
annotation with an explicit response type, and without it springdoc falls back to introspecting
|
||
the handler method's own return type — which for a functional endpoint is always `ServerResponse`,
|
||
not whatever body you happen to construct inside it. That fallback isn't about versioning at all;
|
||
it would happen on an unversioned functional endpoint too.
|
||
|
||
**My best reading of what #3229 actually fixed**, based on this and on the same release's fix
|
||
entry "#3232 – Gracefully handle springdoc endpoint paths during API version resolution": Spring's
|
||
own request-mapping introspection previously had no case for a `version(...)` predicate node in a
|
||
functional route's predicate tree, which made springdoc's *own* endpoints (`/v3/api-docs`,
|
||
`/swagger-ui.html`, etc.) trip over API version resolution once versioning was enabled at all,
|
||
functional endpoints or not. #3229 most plausibly taught springdoc's router-visiting code to walk
|
||
past a `version(...)` predicate node without erroring — not to generate a separate operation per
|
||
version. I can't fully confirm that reading from the outside (the PR's own diff isn't part of the
|
||
release notes text), so treat it as my best inference from the observed behaviour rather than a
|
||
confirmed fact — but the observed behaviour itself (one operation survives, not versioned
|
||
separately) is directly reproduced above and is not in question.
|
||
|
||
**Practical takeaway: if you need real per-version OpenAPI documentation today, the annotated
|
||
`@GetMapping` + `GroupedOpenApi` customizer approach in the next chapter is the one that actually
|
||
works.** The functional-endpoint path is not currently a better-documented alternative for
|
||
versioned APIs, whatever "Add support for ... Functional Endpoints" in the changelog might suggest
|
||
on its own.
|