Skip to content

Derive JSON Schema from Python type hints in OpenAPI output#9

Merged
thorwhalen merged 2 commits into
masterfrom
openapi-json-schema
May 21, 2026
Merged

Derive JSON Schema from Python type hints in OpenAPI output#9
thorwhalen merged 2 commits into
masterfrom
openapi-json-schema

Conversation

@thorwhalen
Copy link
Copy Markdown
Member

Summary

qh's /openapi.json listed routes and docstrings but emitted empty {} request/response schemas and no components.schemas — so openapi-typescript produced no useful types and downstream consumers (e.g. app_ef) had to hand-write their API types.

Root cause: make_endpoint builds every route with a generic endpoint(request: Request) signature. FastAPI generates OpenAPI by introspecting the endpoint signature, so the wrapped function's real parameters and return type — parsed manually from the request body at runtime — are invisible to it.

This PR derives a complete OpenAPI document from the wrapped functions' Python type hints. It is additive: the request-handling path is untouched.

What changed

  • qh/openapi.pypython_type_to_json_schema(): a recursive Python-type-hint → JSON Schema converter. Handles primitives, list/dict/tuple, Optional/Union (incl. PEP 604 X | Y), Literal, Any, dataclasses, TypedDicts (incl. total=False), Pydantic models, NamedTuples, and Enums. Named composite types are registered in components.schemas and referenced via $ref; a recursion guard handles self-referential types.
  • enhance_openapi_schema() now fills requestBody, parameters (path/query), responses.200, and components.schemas.
  • install_enhanced_openapi() overrides app.openapi so the enriched document is served at /openapi.json (and rendered by /docs). Defensive: falls back to FastAPI's plain schema if enhancement ever raises, so a converter bug can never 500 the doc.
  • endpoint.py exposes the resolved param → HTTP-location map; app.py surfaces it via inspect_routes — a single source of truth for body/path/query classification, so openapi.py does not re-derive that logic.
  • mk_app(enhanced_openapi=True) installs it by default.

Interface impact

Interface-preserving and additive. Existing /openapi.json consumers receive a strictly richer document; no request-handling behaviour changes. Opt out with mk_app(..., enhanced_openapi=False).

Tests

  • All 139 existing tests still pass.
  • New qh/tests/test_openapi_schema.py — 27 tests covering the converter (primitives, containers, unions, dataclass/TypedDict/NamedTuple/Enum, self-reference) and the enhanced document (request body, optional params, $ref responses, components.schemas, GET → query parameters, None return, opt-out).

Verified end-to-end against ef.service.EfService: all four result types (CorpusInfo, Segment, SearchHit, ExploreResult) resolve correctly into components.schemas.

Closes #8

qh builds every route with a generic endpoint(request: Request) signature,
so the wrapped function's parameters and return type are invisible to
FastAPI's OpenAPI machinery: /openapi.json had empty {} request/response
schemas and no components.schemas, and openapi-typescript produced no
useful types.

This derives a complete OpenAPI document from the wrapped functions' Python
type hints — additive, with the request-handling path untouched:

- qh/openapi.py: python_type_to_json_schema() converts Python type hints to
  JSON Schema (primitives, list/dict/tuple, Optional/Union incl. PEP 604,
  Literal, Any, dataclasses, TypedDicts incl. total=False, Pydantic models,
  NamedTuples, Enums). Named composites are registered in components.schemas
  and referenced by $ref; the recursion guard handles self-referential types.
- enhance_openapi_schema() now fills requestBody, parameters (path/query),
  responses.200 and components.schemas.
- install_enhanced_openapi() overrides app.openapi to serve the enriched doc
  at /openapi.json, with a defensive fallback to FastAPI's plain schema.
- endpoint.py exposes the resolved param -> HTTP-location map; app.py
  surfaces it via inspect_routes — single source of truth for body/path/query
  classification.
- mk_app(enhanced_openapi=True) installs it by default.

Adds qh/tests/test_openapi_schema.py (27 tests).

Closes #8
get_type_hints leaves the inner type of a self-referential annotation like
list["TreeNode"] unresolved — a bare str on Python 3.10, a ForwardRef
elsewhere. python_type_to_json_schema discarded both, so a self-referential
field's items lost its $ref on 3.10. Resolve a forward-ref str/ForwardRef
against the schema registry and recursion stack instead.

Fixes the test_self_referential_dataclass_terminates failure on Python 3.10.
@thorwhalen thorwhalen merged commit 18c3055 into master May 21, 2026
12 checks passed
@thorwhalen thorwhalen deleted the openapi-json-schema branch May 21, 2026 19:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Derive JSON Schema from Python type hints in OpenAPI output

1 participant