A public API deserves a public contract. So teams write an OpenAPI document, and the day after they write it, the code moves and the document does not. A hand-written spec is a promise you stop keeping almost immediately. The way out is to stop writing it by hand.

Two descriptions cannot stay in sync

The moment you have both the code and a separate spec, you have two descriptions of the same API and a full-time job keeping them equal. Nobody does that job forever. The spec grows stale, consumers trust it less, and eventually it describes an API that no longer exists.

You already wrote the truth once: the validation schemas that decide what each route accepts. The contract should be a view of those, not a parallel artifact you maintain in parallel.

Your schemasvalidationtoJsonSchemaschema → JSON SchemaOpenApiGeneratora valid OpenAPI doc/openapi.jsonserved from a route
The schemas you validate against become the JSON Schema in the document. One source, generated, never a second copy to maintain.

Generate the document

OpenApiGenerator builds a valid OpenAPI document from your app: the info block, the servers, and the paths and schemas taken from your routes, validation and resources. Under the hood toJsonSchema converts your Zod and Standard Schemas into the JSON Schema the document embeds, which is why the contract stays a faithful view of what you actually validate.

app/openapi.tsts
import { OpenApiGenerator } from '@stone-js/openapi'

export const spec = OpenApiGenerator
  .create({ title: 'Tasks API', version: '1.0.0' })
  .addServer('https://api.example.com')
  .build()   // a valid OpenAPI document

Serve it

Expose the built document from a route. Machines, tools and agents read it directly; point any OpenAPI viewer (Swagger UI, Scalar) at that URL for a human-friendly, interactive explorer, so there is no second document and no separate docs site to keep in sync.

app/OpenApiController.tsts
import { EventHandler, Get } from '@stone-js/router'
import { spec } from './openapi'

@EventHandler('/openapi.json')
export class OpenApiController {
  @Get('/')
  document () { return spec }   // the generated OpenAPI document, as JSON
}

Why it matters

  • The contract cannot drift. It is generated from the schemas that already gate your routes, so it is true by construction.
  • Zero second document. You maintain schemas, not a spec, and the spec follows for free on every build.
  • One contract, two readers. The same document that documents your API for humans describes it for machines and agents.

The generator and JSON Schema conversion are covered in OpenAPI. Because the schemas come from Validation, the same objects that reject a bad request are the ones that describe a good one. Paired with the agent-native tooling, an agent can even discover your endpoints from the contract you already generate.