Derive JSON Schema from Python type hints in OpenAPI output#9
Merged
Conversation
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
This was referenced May 21, 2026
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
qh's/openapi.jsonlisted routes and docstrings but emitted empty{}request/response schemas and nocomponents.schemas— soopenapi-typescriptproduced no useful types and downstream consumers (e.g.app_ef) had to hand-write their API types.Root cause:
make_endpointbuilds every route with a genericendpoint(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.py—python_type_to_json_schema(): a recursive Python-type-hint → JSON Schema converter. Handles primitives,list/dict/tuple,Optional/Union(incl. PEP 604X | Y),Literal,Any, dataclasses,TypedDicts (incl.total=False), Pydantic models,NamedTuples, andEnums. Named composite types are registered incomponents.schemasand referenced via$ref; a recursion guard handles self-referential types.enhance_openapi_schema()now fillsrequestBody,parameters(path/query),responses.200, andcomponents.schemas.install_enhanced_openapi()overridesapp.openapiso 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.pyexposes the resolved param → HTTP-location map;app.pysurfaces it viainspect_routes— a single source of truth for body/path/query classification, soopenapi.pydoes not re-derive that logic.mk_app(enhanced_openapi=True)installs it by default.Interface impact
Interface-preserving and additive. Existing
/openapi.jsonconsumers receive a strictly richer document; no request-handling behaviour changes. Opt out withmk_app(..., enhanced_openapi=False).Tests
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,$refresponses,components.schemas, GET → query parameters,Nonereturn, opt-out).Verified end-to-end against
ef.service.EfService: all four result types (CorpusInfo,Segment,SearchHit,ExploreResult) resolve correctly intocomponents.schemas.Closes #8