ADR 0018 — OpenAPI generated from the contract
- Status: Accepted
- Date: 2026-05-29
Context
A defineContract already carries the full type information for the HTTP
surface — Zod params / input / output, desc, method, path, scope.
An OpenAPI document is that information re-expressed. Hand-maintaining a separate
spec (or decorating handlers) duplicates the contract and drifts from it.
The CLI transport’s --help walker (ADR 0016) needs the same thing the spec
needs: a Zod → JSON Schema conversion and a way to read an object schema’s
top-level properties. Building two divergent walkers would be the duplication
rule’s exact failure mode.
Decision
Generate OpenAPI 3.1 from contract services; share the schema traversal with
the rest of the framework. generateOpenApiDocument({ info, services, groups })
returns a { openapi: '3.1.0', info, paths } document.
- A method is included when it is HTTP-exposed —
exposeabsent, orexposeincludes'HTTP'— the same rule the router uses to build routes, so the spec and the live routes never disagree about what exists. pathparams come fromparamsSchema(in: 'path'); a GET’sinputSchemabecomes query parameters; a non-GET’sinputSchemabecomes a JSONrequestBody;outputSchemabecomes the200response;scope(when notpublic) adds401/403.- Conversion goes through the single
toJsonSchemapoint and the sharedjsonSchemaFieldshelper — the same two functions the CLI--helpwalker uses. An unrepresentable construct degrades to{}rather than throwing the whole document, mirroringbuildToolManifest. openApiRoute('/openapi.json', doc)is aRawRoutethat serves the document.
Schemas are inlined, not de-duplicated into components/$ref. Inlined output
is valid OpenAPI and keeps v1 simple; $ref de-dup can come later if a spec
grows unwieldy.
Consequences
- The OpenAPI spec is free and always in sync — the contract is the single source, no decorators, no parallel document.
- The schema-introspection code has one home:
tools/json-schema.tspowers the MCP manifest, the CLI--helptable and the OpenAPI document alike. - Typed path-param inference from the path literal (Hono-style) and
$refde-duplication are explicitly deferred.