Skip to main content

hayate-openapi

Hayate ecosystem: Start here · Production golden app · Tested compatibility

OpenAPI 3.1 generation for hayate — built from what your app already knows: routes from app.routes, request schemas from validators or explicit typed parameters, and response schemas from one decorator. No implicit body/query guessing and no schema-library lock-in.

Status: alpha (0.8.x). The emitted OpenAPI 3.1.1 document passes openapi-spec-validator and feeds openapi-typescript 7.13 for end-to-end TypeScript types in a locked CI interoperability gate. The internal design memo (Japanese, per project convention) lives in DESIGN.md. Interactive Scalar docs, security schemes, multipart uploads, and strict inline typing are included. Release history is in CHANGELOG.md.

The typed endpoint surface removes repeated validation and response glue while keeping every request source explicit:

from typing import Annotated, TypedDict
from uuid import UUID

from hayate import Hayate
from hayate_openapi import Body, Constraints, OpenApi, Path, Query, endpoint


class BookIn(TypedDict):
    title: str


class BookOut(TypedDict):
    id: UUID
    title: str


app = Hayate()


@app.post("/books/:book_id")
@endpoint(status=201, summary="Create a book")
async def create(
    book_id: Annotated[UUID, Path()],
    book: Annotated[BookIn, Body()],
    notify: Annotated[bool, Query()] = False,
    limit: Annotated[int, Constraints(ge=1, le=100), Query()] = 25,
) -> BookOut:
    if notify:
        ...
    return {"id": book_id, "title": book["title"]}


OpenApi(app, title="Bookstore", version="1.0.0").register(app)

@endpoint validates and converts every marked input, validates the declared return type, emits JSON, and attaches the exact same schemas to OpenAPI. Invalid inputs use Hayate's RFC 9457 400 response; response contract failures remain non-leaking 500 errors. Put the decorator closest to the function, below the Hayate route decorator.

Constraints supplies portable numeric bounds (gt, ge, lt, le) and string rules (min_length, max_length, pattern) without Pydantic or msgspec. The same metadata validates converted HTTP values and emits JSON Schema keywords, including on the Cloudflare Workers/Pyodide path.

Dependencies can have request parameters and sub-dependencies. A dependency runs once per request even when several consumers use it:

from hayate import Context
from hayate_openapi import Depends, Header


def request_id(
    c: Context,
    value: Annotated[UUID, Header(alias="x-request-id")],
) -> UUID:
    return value


@app.get("/jobs/:job_id")
@endpoint
async def job(
    job_id: Annotated[UUID, Path()],
    trace: Annotated[UUID, Depends(request_id)],
) -> dict[str, str]:
    return {"job_id": str(job_id), "trace": str(trace)}

Use Depends(callable, use_cache=False) only when a fresh result is required. Synchronous dependencies run in a worker thread on native Python and inline on threadless Pyodide; async dependencies work on both.

The original Context-first surface remains fully supported:

from hayate import Hayate
from hayate_openapi import OpenApi, describe, validated
import msgspec


class BookIn(msgspec.Struct):
    title: str


app = Hayate()


@app.post("/books", validated("json", BookIn))  # validator + schema tag in one
@describe(status=201, summary="Create a book")
async def create(c):
    book = c.req.valid("json")  # BookIn instance — validation still runs
    return c.json({"title": book.title}, status=201)


OpenApi(app, title="Bookstore", version="1.0.0").register(app)
# GET /openapi.json and interactive GET /docs are live; or emit statically:
#   python -m hayate_openapi main:app --title Bookstore --version 1.0.0

How it works

Source What it provides
app.routes (hayate ≥ 0.8) every method + path, converted to OpenAPI templating (:id → {id})
@endpoint + Annotated markers typed binding, dependencies, response validation, JSON serialization, and matching schemas
validated(target, T) request body plus query / path / header / cookie schemas — a tagging wrapper around the core validator, behavior-identical
@describe(...) summary, tags, response schemas, operationId — all optional, all additive

hayate-auth middleware can supply operation security automatically:

@app.get("/documents", auth.require_oauth_token("documents:read"))
async def documents(c):
    return c.json([])


OpenApi(
    app,
    title="API",
    version="1",
    security_schemes=auth.openapi_security_schemes(),
).register(app)

Use @describe(security=[]) for an explicitly public operation. For uploads, use Hayate's own File type in a typed multipart field:

from hashlib import sha256
from typing import Annotated

from hayate import File, FormDataLimits
from hayate_openapi import Form, endpoint

UPLOAD_LIMITS = FormDataLimits(
    max_body_bytes=10 * 1024 * 1024,
    max_file_bytes=10 * 1024 * 1024,
    file_memory_bytes=1024 * 1024,
)


@app.post("/uploads")
@endpoint(status=201)
async def upload(
    file: Annotated[
        File,
        Form(media_type="multipart/form-data", limits=UPLOAD_LIMITS),
    ],
) -> dict[str, str | int]:
    digest = sha256()
    async for chunk in file.stream():
        digest.update(chunk)
    return {"name": file.name, "size": file.size, "sha256": digest.hexdigest()}

The same declaration binds the runtime File, emits an OpenAPI string/binary part, applies parser limits, and documents both 400 and 413 failures. On native streamed requests, files above file_memory_bytes spill to an unnamed temporary file; Workers remain disk-free and bounded. @endpoint closes parsed files after the handler returns or raises, so consume or persist them during the handler call. All typed fields in one form, including fields declared by dependencies, must share one media type and one FormDataLimits value.

The Context-first surface remains available: combine validated("form", schema, media_type="multipart/form-data") with binary_file() in a raw schema.

Schema conversion goes through a SchemaProvider protocol. The built-in StdlibProvider covers primitives, UUID/date/time, enums, literals, unions, collections, mappings, and TypedDict without another dependency. msgspec and pydantic are auto-detected through guarded imports; install them with hayate-openapi[msgspec] or hayate-openapi[pydantic] when their models and constraints are desired. A plain dict is taken as literal JSON Schema Draft 2020-12 and compiled once for runtime validation, including supported format checks such as UUID. CPython checks raw schemas when routes register. Pyodide compiles them on the first matching request and caches the validator because Workers forbids extension entropy during module-global evaluation. Raw schemas validate the literal request representation, so query, path, header, and cookie values are strings. Use @endpoint with typed parameters when the wire value should be converted to an integer, number, or boolean.

validated() supports the core's six targets: json, form, query, param, header, and cookie. The last four expand object properties into OpenAPI parameters. A param property must match a route parameter and overrides its generated schema while remaining required as OpenAPI mandates. Header schemas use the lowercase names exposed by Fetch; model aliases such as x-request-id keep a Python field name separate when needed. OpenAPI-reserved Accept, Content-Type, and Authorization header parameters are rejected instead of emitting fields the specification ignores.

Generate TypeScript types and the first-party callable client:

python -m hayate_openapi main:app \
  --title API \
  --version 1.0.0 \
  --output openapi.json \
  --typescript-client src/api-client.ts \
  --typescript-types-import ./api-types.js
npx openapi-typescript openapi.json -o src/api-types.ts

The generated client imports only openapi-typescript's paths type. Its runtime has no package dependency: it uses the platform Fetch, URL, Headers, URLSearchParams, FormData, and Blob APIs. Every documented operation becomes a method keyed by its operationId:

import { createHayateClient } from "./api-client.js";

const api = createHayateClient({ baseUrl: "https://api.example.com/v1" });
const response = await api.post_typed_books_book_id({
  path: { book_id: "018f47a6-42d2-7f5a-a724-35d7d230ad42" },
  query: { notify: true },
  json: { title: "Typed contracts" },
});

if (response.status === 201) {
  const book = await response.json(); // status-discriminated response type
}

Path, query, header, cookie, JSON, URL-encoded, and multipart values are typed from the emitted OpenAPI document. Generation fails when a document asks for a serialization the runtime does not implement, including ambiguous request media types, object-valued parameters, custom form encodings, nested form values, or non-JSON response bodies.

Cookie parameters are for server-side Fetch runtimes. Browsers control the Cookie header themselves; use credentials for browser-managed cookies. Cookie arrays are rejected because OpenAPI 3.1 Appendix D notes that form-style multi-value serialization does not match the semicolon-delimited Cookie header.

The repository continuously validates, generates, strictly compiles, and runs the same flow against a representative application:

npm ci --ignore-scripts
scripts/check_interoperability.sh

That gate starts a real Hayate ASGI process and executes path/query/JSON, multipart, and form/header/cookie round trips with the compiled generated client.

The generator deliberately emits OpenAPI 3.1.1. Although OpenAPI 3.2.0 is published, openapi-typescript documents support for 3.0 and 3.1. Staying on 3.1.1 keeps validation, documentation, and typed client generation on one tested interoperability profile instead of claiming an unverified version bump. The private Node lock overrides vulnerable transitive parser/glob versions with patched releases; it is CI tooling and is not part of the Python distribution.

Interactive API reference

register() serves a Scalar API reference at /docs by default. It can execute requests, render schemas and security requirements, and generate client examples directly from the same OpenAPI 3.1 document.

The page has no inline JavaScript. It pins Scalar to an immutable version with Subresource Integrity and sends a restrictive Content Security Policy. Scalar is loaded from jsDelivr at browser time, while Scalar's telemetry, external client, sharing, deployment, MCP-generation, developer-tools, and AI-agent integrations are disabled by default. The Python package therefore keeps its single hayate dependency without sending the API document to another service. Disable the page or self-host the script when needed:

# JSON only
OpenApi(app, title="API", version="1", docs_path=None).register(app)

# Same-origin, self-hosted Scalar bundle
OpenApi(
    app,
    title="API",
    version="1",
    scalar_script_url="/assets/scalar.js",
).register(app)

The OpenAPI JSON and docs routes are intentionally excluded from the generated application schema.

What is documented (and what is not)

  • Routes with real HTTP verbs; WebSocket routes and wildcard mounts (/api/auth/*) are skipped.
  • Responses you declare. Undeclared operations get a bare 200 — the generator never invents schemas.
  • Operations with a validator automatically document the 400 application/problem+json failure the framework actually returns; form operations also document parser-limit failures as 413.
  • Cookie, Bearer, and OAuth 2.0 security requirements, including combined route middleware requirements.
  • JSON, URL-encoded, and multipart request bodies, including typed binary File parts with shared resource limits.
  • Explicit typed endpoint parameters, including request fields declared by cached sub-dependencies, and validated return annotations.

License

MIT

Metadata

Release files for hayate-openapi 0.8.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for hayate-openapi 0.8.2
File Size Uploaded Provenance
hayate_openapi-0.8.2.tar.gz 31.6 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for hayate-openapi 0.8.2
File Interpreter ABI Platform
hayate_openapi-0.8.2-py3-none-any.whl Python 3 none any Details

Total release size: 67.2 kB

Release files / hayate_openapi-0.8.2.tar.gz

Download URL hayate_openapi-0.8.2.tar.gz
Size 31.6 kB
Tags Source
SHA-256 checksum
How to use checksums
47ba5dd244ba8750b8205ba7c159286231edfb704c4a477ca62c46df5ace1950
BLAKE2b-256 checksum
How to use checksums
589fe4d39fe44e7952ab0ccc0a124962416e31a150b9390eb54a5f9aef9bff02
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 30, 2026.

Transparency log

Release files / hayate_openapi-0.8.2-py3-none-any.whl

Download URL hayate_openapi-0.8.2-py3-none-any.whl
Size 35.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d7b5cf9ea0104b826d212c3c5739281c03a174255cc7f2b7494c0a7409f75585
BLAKE2b-256 checksum
How to use checksums
068e16dda9347e3888e04c67c7988734eb85567aafdfd5ca0f6031e6b672df02
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 30, 2026.

Transparency log

Security for hayate-openapi 0.8.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for hayate_openapi-0.8.2.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 30, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page