Files
spring-boot-demo/openapi-versioning/docs/02-what-springdoc-actually-generates.md
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

3.5 KiB

What springdoc actually generates for a versioned path

Full transcript: docs/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"):

{
  "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 HandlerMethods 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 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 instead.