The API reference, generated from the code in a test
The OpenAPI document is generated from the application in a test, completed with what the code enforces and with recorded responses, and rendered by the site. springdoc never ships.
Context
The API is public (ADR 0006): the lab's pages call it, and so can anyone. A
reference written by hand drifts from the code the first time an endpoint changes. One served by the application,
springdoc's /v3/api-docs with Swagger UI, ships a library, the Jackson 2 it brings and endpoints of its own in a
service that scales to zero and counts its startup (ADR 0009), and a third-party
interface the Content-Security-Policy would have to let in. And the facts a client needs most, the errors an
operation answers with, its rate limit and the version header, are not in the controller signatures springdoc reads.
Decision
- Generated in a test, not served. springdoc-openapi is a test dependency.
ApiReferenceTeststarts the application, takes springdoc's document and compares it withweb/src/content/openapi.json, failing while they differ;./gradlew exportApiReferencewrites the file. Each endpoint is described where it is declared, with@Tag,@Operationand@Parameter, annotations that are compile-only and absent from the jar. - What the code enforces, added from the code. Refinements in the test complete the document: the RFC 9457
problem and each operation's codes by status, the shared ones derived from the version header, the rate limit and
each parameter's constraints, and the particular ones listed, where an operation missing from the list fails the
export; the
API-Versionheader from the configured versioning; each limited operation's policy and the draft-ietf-httpapi-ratelimit-headers-11 headers fromapp.rate-limit.*; server-sent events astext/event-stream; records as Jackson writes them, every component present and the@Nullableones nullable; patterns anchored as Bean Validation matches them; nested types named as the code spells them,ProfileReport.Gc. - Examples recorded, not written.
scripts/record-examples.mjssaves one real response per operation from a running API. The test cuts long arrays and long streams short and says so in the example, checks every example against the schema the code generates, so a field added or renamed fails, and replays the recorded problem against the application. - Rendered by the site.
/api-reference/renders the document with the site's own components, prerendered like every page and without third-party scripts; the document itself is published at/api-reference/openapi.jsonfor tools. - Versioned by header, limited in the open. Spring Framework 7's API versioning reads
API-Version, version 1 when it is absent and the only one supported; every response to a limited operation says where the client stands in its policy.
Consequences
- A new endpoint, a changed parameter, a new code in the list or a changed limit shows up as a diff of
openapi.jsonin the same change; one that is not exported fails the build. - Generating it found two schemas that had been merged by name: the profile's and the telemetry sample's
Gc, the ledger's and the outbox'sVerdict. A reference served by springdoc as it comes would have described one with the other's fields, and read as right. - Recordings age. A response that changes shape fails the schema check until
record-examples.mjsruns again against a current build, which takes Docker, the lab's PostgreSQL and about two and a half minutes. - The particular codes are a list kept by hand and checked for completeness, not derived: a code path that starts answering with a new code is caught by review, not by the test.
- OpenAPI 3.1 cannot describe the items of a stream (3.2 can), so the events are named in each operation's description and shown in a recorded stream.