Skip to content

Commit 9f7ba04

Browse files
committed
Fix SQLAlchemy JSON array emission, FastAPI Pydantic-safe route annotations, and CLI loads import
- Map OpenAPI 'array' properties ('list' from map_openapi_type_to_python) to 'dict' in openapi_client.sqlalchemy_cdd.emit.emit_sqlalchemy so cdd.emit.sqlalchemy.sqlalchemy emits Column(JSON, default=None) instead of invalid Column(list, default=None) that triggers sqlalchemy.exc.ArgumentError at declarative model import time. - Introduce _to_fastapi_type in openapi_client.fastapi.emit to normalize component $ref and array-of-$ref schema annotations into Pydantic-safe JSON structures (dict[str, Any] and list[dict[str, Any]]) across route parameters, requestBody, and response_model return annotations, preventing fastapi.exceptions.FastAPIError when generated server models are SQLAlchemy declarative_base() ORM classes. - Emit 'from json import loads' in openapi_client.cli_sdk_cdd.emit.emit_cli_sdk so cdd.emit.argparse_function's type=loads for dict/requestBody CLI flags resolves at runtime. - Decorate test_stub_endpoints in openapi_client.tests.emit_server_tests with @patch.dict(os.environ, {}, clear=True) to isolate stub-mode endpoint tests from ambient DATABASE_URL or EPHEMERAL_DB variables.
1 parent 362c38c commit 9f7ba04

6 files changed

Lines changed: 42 additions & 14 deletions

File tree

‎src/openapi_client/cli_sdk_cdd/emit.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ def emit_cli_sdk(spec: OpenAPI) -> str:
2626
body = [
2727
"import argparse",
2828
"import json",
29+
"from json import loads",
2930
"import os",
3031
"import sys",
3132
"from client import Client",

‎src/openapi_client/fastapi/emit.py‎

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,27 @@ def _match_model_for_tag(tag: str, spec: OpenAPI) -> str | None:
4040
return None
4141

4242

43+
def _to_fastapi_type(ann: str) -> str:
44+
"""Normalize a schema type annotation string into a Pydantic-safe FastAPI type.
45+
46+
Since generated server `models` are SQLAlchemy declarative ORM classes rather than
47+
Pydantic `BaseModel` classes, component `$ref` model names are normalized to
48+
`dict[str, Any]` (and `list[<Model>]` to `list[dict[str, Any]]`).
49+
50+
Args:
51+
ann: Raw annotation string returned by `get_annotation_for_schema`.
52+
53+
Returns:
54+
A valid Pydantic/FastAPI type annotation string.
55+
"""
56+
if ann in ("Any", "str", "int", "float", "bool", "dict[str, Any]"):
57+
return ann
58+
if ann.startswith("list[") and ann.endswith("]"):
59+
inner = _to_fastapi_type(ann[5:-1])
60+
return f"list[{inner}]"
61+
return "dict[str, Any]"
62+
63+
4364
def _extract_request_body_type(operation: Operation) -> str:
4465
"""Resolve the type annotation string for an operation's requestBody.
4566
@@ -54,10 +75,8 @@ def _extract_request_body_type(operation: Operation) -> str:
5475
if isinstance(content, dict) and content:
5576
media = content.get("application/json") or next(iter(content.values()))
5677
schema_obj = getattr(media, "schema_", getattr(media, "schema", None))
57-
ann = get_annotation_for_schema(schema_obj)
58-
if ann and ann != "Any":
59-
if not ann.startswith(("dict", "list", "str", "int", "float", "bool")):
60-
return f"models.{ann} | dict[str, Any] | None"
78+
ann = _to_fastapi_type(get_annotation_for_schema(schema_obj))
79+
if ann != "Any":
6180
return f"{ann} | None"
6281
return "dict[str, Any] | None"
6382

@@ -81,13 +100,11 @@ def _extract_response_type(operation: Operation) -> str | None:
81100
media = content.get("application/json") or next(iter(content.values()))
82101
schema_obj = getattr(media, "schema_", getattr(media, "schema", None))
83102
if schema_obj is not None:
84-
ann = get_annotation_for_schema(schema_obj)
85-
if ann and ann != "Any":
86-
if not ann.startswith(
87-
("dict", "list", "str", "int", "float", "bool")
88-
):
89-
return f"models.{ann} | dict[str, Any] | None"
90-
return f"{ann} | dict[str, Any] | None"
103+
ann = _to_fastapi_type(get_annotation_for_schema(schema_obj))
104+
if ann != "Any":
105+
if ann == "dict[str, Any]":
106+
return "dict[str, Any] | list[Any] | None"
107+
return f"{ann} | dict[str, Any] | list[Any] | None"
91108
return None
92109

93110

@@ -123,7 +140,7 @@ def _build_route_function(
123140
if safe_p and safe_p not in seen_params:
124141
seen_params.add(safe_p)
125142
schema_obj = getattr(param, "schema_", getattr(param, "schema", None))
126-
ann_str = get_annotation_for_schema(schema_obj)
143+
ann_str = _to_fastapi_type(get_annotation_for_schema(schema_obj))
127144
params.append(
128145
cst.Param(
129146
name=cst.Name(safe_p),

‎src/openapi_client/sqlalchemy_cdd/emit.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@ def emit_sqlalchemy(spec: OpenAPI) -> dict[str, str]:
5252
if prop_name.lower() == "id":
5353
doc = f"[PK] {doc}".strip()
5454

55+
mapped_type = map_openapi_type_to_python(str(p_type))
5556
params[prop_name] = {
56-
"typ": map_openapi_type_to_python(str(p_type)),
57+
"typ": "dict" if mapped_type == "list" else mapped_type,
5758
"doc": doc,
5859
"default": None,
5960
}

‎src/openapi_client/tests/emit_server_tests.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,12 @@ def emit_server_tests(spec: OpenAPI) -> dict[str, str]:
114114
files["test_unit.py"] = "\n".join(unit_lines)
115115

116116
stub_lines = [
117+
"import os",
118+
"from unittest.mock import patch",
117119
"from fastapi.testclient import TestClient",
118120
"from main import app",
119121
"",
122+
"@patch.dict(os.environ, {}, clear=True)",
120123
"def test_stub_endpoints():",
121124
' """Test that endpoints return NotImplemented safely in stub mode."""',
122125
" client = TestClient(app)",

‎tests/test_dao_db_seeder.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,7 @@ def test_execute_generated_dao_db_seeder_end_to_end(
494494
"id": Schema(type="integer"),
495495
"name": Schema(type="string"),
496496
"email": Schema(type="string"),
497+
"roles": Schema(type="array", items=Schema(type="string")),
497498
},
498499
),
499500
}

‎tests/test_fastapi.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -751,7 +751,12 @@ def include_router(self, router: APIRouter) -> None:
751751
operationId="replace_users",
752752
requestBody=RequestBody(
753753
content={
754-
"application/json": MediaType(schema=Schema(type="object"))
754+
"application/json": MediaType(
755+
schema=Schema(
756+
type="array",
757+
items=Reference(ref="#/components/schemas/User"),
758+
)
759+
)
755760
}
756761
),
757762
responses={"200": Response(description="No content", content={})},

0 commit comments

Comments
 (0)