OpenAPI & API Docs¶
zeython.openapi generates an OpenAPI 3.0 document from the routes
actually registered on your app, and serves it alongside an interactive
Swagger UI. zeython new wires this in by default — visit /docs on a
running app.
What this is, and what it deliberately isn't¶
This is not FastAPI-style automatic request/response validation from
type hints. A Zeython handler takes a plain request and calls
request.json() itself; a model validates via __rules__
(see Validation), not a typed request model. Rearchitecting
that to get automatic schema inference the way FastAPI does would be a much
bigger, separate change — and would fight the framework's existing
conventions rather than describe them.
What this module actually does: read the routes registered on
app.router (the same technique the AI Agents MCP server's
list_routes uses) and build an OpenAPI document from them. Every route
appears, even with no annotation at all — with a generic "Successful
response" 200. The optional @describe(...) decorator is how you give a
route a real summary, tags, or request/response schema instead.
Setup¶
Registered by default in a generated project:
Visit /docs for the Swagger UI, /openapi.json for the raw spec.
Describing a route¶
from zeython.openapi import describe, model_schema
from app.Models.post import Post
_POST_SCHEMA = model_schema(Post)
class PostController(Controller):
@describe(
summary="List posts",
tags=["posts"],
responses={200: {
"description": "The post list",
"content": {"application/json": {"schema": {"type": "array", "items": _POST_SCHEMA}}},
}},
)
async def index(self, request): ...
@describe works on both function-based routes and Controller methods —
it attaches to the underlying function, and Python's bound-method attribute
lookup falls through to it automatically, so it survives being wrapped as
instance.index inside Router.resource().
model_schema()¶
A convenience for building the JSON Schema fragments @describe takes,
from a model's actual mapped columns:
model_schema(Post)
# {"type": "object", "properties": {"id": {"type": "integer"}, "title": {"type": "string"}, ...}, "required": [...]}
Automatically excludes __hidden__ fields (password_hash, etc.) — the
same fields to_dict() never serializes shouldn't show up as "here's the
shape of this response" either. This is documentation, generated once when
the module loads — nothing checks an actual response body against it at
request time.
Configuration¶
OPENAPI_ENABLED=true # false to register the provider without exposing the routes
OPENAPI_JSON_PATH=/openapi.json
OPENAPI_DOCS_PATH=/docs
Set OPENAPI_ENABLED=false to keep generate_openapi() usable
programmatically (e.g. to write openapi.json to a file as part of a
build step) without exposing /docs//openapi.json over HTTP — a
reasonable choice in production if you don't want your API surface
publicly browsable.
Swagger UI loads from a CDN (swagger-ui-dist), same zero-setup approach
as the Tailwind welcome page — but unlike Tailwind's Play
CDN, this is a static asset bundle, not something recompiled on every
page load, so there's no dev-only caveat here.
Scope limits¶
- Routes mounted via
Router.mount()/.include()(StaticFiles, a nested sub-router) aren't recursed into and don't appear in the spec — directly registered routes only. - Path parameters get a schema from a Starlette type converter if you used
one (
{id:int}→integer); an untyped{id}defaults tostring. - There's no request-body validation here, only documentation — the
request_body/responsesyou pass to@describedescribe what your handler is supposed to do, and nothing enforces that it matches.