Skip to content

Commit b9eedbc

Browse files
committed
Derive JSON Schema from Python type hints in OpenAPI output
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
1 parent 8ffb74d commit b9eedbc

5 files changed

Lines changed: 901 additions & 46 deletions

File tree

qh/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@
2323
from qh.types import register_type, register_json_type, TypeRegistry
2424

2525
# OpenAPI and client generation (Phase 3)
26-
from qh.openapi import export_openapi, enhance_openapi_schema
26+
from qh.openapi import (
27+
export_openapi,
28+
enhance_openapi_schema,
29+
install_enhanced_openapi,
30+
python_type_to_json_schema,
31+
)
2732
from qh.client import (
2833
mk_client_from_openapi,
2934
mk_client_from_url,
@@ -117,6 +122,8 @@
117122
# OpenAPI & Client (Phase 3)
118123
"export_openapi",
119124
"enhance_openapi_schema",
125+
"install_enhanced_openapi",
126+
"python_type_to_json_schema",
120127
"mk_client_from_openapi",
121128
"mk_client_from_url",
122129
"mk_client_from_app",

qh/app.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def mk_app(
3232
use_conventions: bool = False,
3333
async_funcs: Optional[List[Union[str, Callable]]] = None,
3434
async_config: Optional[Union[Dict[str, Any], "TaskConfig"]] = None,
35+
enhanced_openapi: bool = True,
3536
**kwargs,
3637
) -> FastAPI:
3738
"""
@@ -69,6 +70,12 @@ def mk_app(
6970
- TaskConfig object (applies to all async_funcs)
7071
- Dict mapping function names to TaskConfig objects
7172
73+
enhanced_openapi: Whether to serve an enhanced OpenAPI document at
74+
``/openapi.json`` — one with ``requestBody`` / ``responses`` /
75+
``components.schemas`` derived from each function's Python type
76+
hints (see :mod:`qh.openapi`). Defaults to True; the enhancement is
77+
additive and falls back to FastAPI's plain schema if it ever fails.
78+
7279
**kwargs: Additional FastAPI() constructor kwargs (if creating new app)
7380
7481
Returns:
@@ -254,6 +261,13 @@ def mk_app(
254261
if task_config and getattr(task_config, "create_task_endpoints", True):
255262
add_task_endpoints(app, func.__name__)
256263

264+
# Serve an OpenAPI document with full request/response JSON Schema derived
265+
# from the wrapped functions' Python type hints.
266+
if enhanced_openapi:
267+
from qh.openapi import install_enhanced_openapi
268+
269+
install_enhanced_openapi(app)
270+
257271
return app
258272

259273

@@ -280,6 +294,10 @@ def inspect_routes(app: FastAPI) -> List[Dict[str, Any]]:
280294
# Include original function if available (for OpenAPI/client generation)
281295
if hasattr(route.endpoint, "_qh_original_func"):
282296
route_info["function"] = route.endpoint._qh_original_func
297+
# Include the resolved param -> TransformSpec map (HTTP-location
298+
# classification) for OpenAPI request/response schema generation.
299+
if hasattr(route.endpoint, "_qh_param_specs"):
300+
route_info["param_specs"] = route.endpoint._qh_param_specs
283301
routes.append(route_info)
284302

285303
return routes

qh/endpoint.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,12 @@ def task_wrapper(**kwargs):
312312
endpoint.__doc__ = func.__doc__
313313
# Store original function for OpenAPI/client generation
314314
endpoint._qh_original_func = func # type: ignore
315+
# Store the resolved param -> TransformSpec map (the single source of truth
316+
# for which HTTP location each parameter is read from). OpenAPI generation
317+
# (qh.openapi) consumes this to classify body/path/query parameters without
318+
# re-deriving the logic.
319+
endpoint._qh_param_specs = param_specs # type: ignore
320+
endpoint._qh_route_config = route_config # type: ignore
315321

316322
return endpoint
317323

0 commit comments

Comments
 (0)