Skip to content

Commit 916faf0

Browse files
committed
- Replaced json.dumps/json.loads with pydantic_core to_json/from_json
- `_serialize_response`: model list mapping now handled by Pydantic instead of manual recursion
1 parent cbd3d06 commit 916faf0

File tree

12 files changed

+90
-81
lines changed

12 files changed

+90
-81
lines changed

CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@ FastOpenAPI follows the [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
66

77
## [0.7.0] - Unreleased
88

9+
### Changed
10+
- Replaced `json.dumps/json.loads` with pydantic_core `to_json/from_json`
11+
- `_serialize_response`: model list mapping now handled by Pydantic instead of manual recursion
12+
913
### Fixed
10-
- Fixed issue with parsing repeated query parameters in URL.
14+
- Issue with parsing repeated query parameters in URL.
1115

1216
### Removed
13-
- Removed the `use_aliases` from `BaseRouter` and reverted changes from 0.6.0.
17+
- The `use_aliases` from `BaseRouter` and reverted changes from 0.6.0.
1418

1519
## [0.6.0] – 2025‑04‑16
1620

fastopenapi/base_router.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,8 @@ def _register_docs_endpoints(self):
405405
def _serialize_response(cls, result: Any) -> Any:
406406
if isinstance(result, BaseModel):
407407
return result.model_dump(by_alias=True)
408+
if isinstance(result, list) and result and isinstance(result[0], BaseModel):
409+
return [item.model_dump(by_alias=True) for item in result]
408410
if isinstance(result, list):
409411
return [cls._serialize_response(item) for item in result]
410412
if isinstance(result, dict):

fastopenapi/routers/falcon.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import inspect
2-
import json
32
from collections.abc import Callable
43
from http import HTTPStatus
54

65
import falcon.asgi
6+
from pydantic_core import from_json
77

88
from fastopenapi.base_router import BaseRouter
99

@@ -110,7 +110,7 @@ async def _read_body(self, req):
110110
try:
111111
body_bytes = await req.bounded_stream.read()
112112
if body_bytes:
113-
return json.loads(body_bytes.decode("utf-8"))
113+
return from_json(body_bytes.decode("utf-8"))
114114
except Exception:
115115
pass
116116
return {}

fastopenapi/routers/starlette.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import functools
22
import inspect
3-
import json
43
from collections.abc import Callable
54

5+
from pydantic_core import from_json
66
from starlette.applications import Starlette
77
from starlette.responses import HTMLResponse, JSONResponse
88
from starlette.routing import Route
@@ -25,7 +25,7 @@ async def _starlette_view(cls, request, router, endpoint):
2525
try:
2626
body_bytes = await request.body()
2727
if body_bytes:
28-
body = json.loads(body_bytes.decode("utf-8"))
28+
body = from_json(body_bytes.decode("utf-8"))
2929
except Exception:
3030
pass
3131
all_params = {**query_params, **request.path_params}

fastopenapi/routers/tornado.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22
import re
33
from collections.abc import Callable
44

5-
from tornado.escape import json_decode, json_encode
5+
from pydantic_core import from_json, to_json
66
from tornado.web import Application, RequestHandler, url
77

88
from fastopenapi.base_router import BaseRouter
99

1010

11+
def json_encode(data):
12+
return to_json(data).decode("utf-8").replace("</", "<\\/")
13+
14+
1115
class TornadoDynamicHandler(RequestHandler):
1216
"""
1317
A dynamic request handler for Tornado, which resolves endpoint parameters and
@@ -26,7 +30,7 @@ def initialize(self, **kwargs):
2630
async def prepare(self):
2731
if self.request.body:
2832
try:
29-
self.json_body = json_decode(self.request.body)
33+
self.json_body = from_json(self.request.body)
3034
except Exception:
3135
self.json_body = {}
3236
else:

tests/aiohttp/test_aiohttp_integration.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import json
2-
31
import pytest
2+
from pydantic_core import to_json
43

54

65
class TestAioHttpIntegration:
@@ -61,7 +60,9 @@ async def test_create_item(self, client):
6160
"""Test creating an item"""
6261
new_item = {"name": "New Item", "description": "New Description"}
6362
headers = {"Content-Type": "application/json"}
64-
resp = await client.post("/items", data=json.dumps(new_item), headers=headers)
63+
resp = await client.post(
64+
"/items", data=to_json(new_item).decode("utf-8"), headers=headers
65+
)
6566
assert resp.status == 201
6667
data = await resp.json()
6768
assert data["id"] == 3
@@ -73,7 +74,9 @@ async def test_create_item_incorrect(self, client):
7374
"""Test creating an item with invalid data"""
7475
new_item = {"name": None, "description": "New Description"}
7576
headers = {"Content-Type": "application/json"}
76-
resp = await client.post("/items", data=json.dumps(new_item), headers=headers)
77+
resp = await client.post(
78+
"/items", data=to_json(new_item).decode("utf-8"), headers=headers
79+
)
7780
assert resp.status == 422
7881
data = await resp.json()
7982
assert "Validation error for parameter" in data["error"]["message"]
@@ -93,7 +96,7 @@ async def test_update_item(self, client):
9396
update_data = {"name": "Updated Item", "description": "Updated Description"}
9497
headers = {"Content-Type": "application/json"}
9598
resp = await client.put(
96-
"/items/2", data=json.dumps(update_data), headers=headers
99+
"/items/2", data=to_json(update_data).decode("utf-8"), headers=headers
97100
)
98101
assert resp.status == 200
99102
data = await resp.json()

tests/falcon/test_falcon_integration.py

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import json
2-
31
import pytest
2+
from pydantic_core import from_json, to_json
43

54

65
class TestFalconIntegration:
@@ -10,7 +9,7 @@ def test_get_items_sync(self, sync_client):
109
response = sync_client.simulate_get("/items-sync")
1110

1211
assert response.status_code == 200
13-
result = json.loads(response.text)
12+
result = from_json(response.text)
1413
assert len(result) == 2
1514
assert result[0]["name"] == "Item 1"
1615
assert result[1]["name"] == "Item 2"
@@ -21,7 +20,7 @@ async def test_get_items(self, async_client):
2120
response = await async_client.simulate_get("/items")
2221

2322
assert response.status_code == 200
24-
result = json.loads(response.text)
23+
result = from_json(response.text)
2524
assert len(result) == 2
2625
assert result[0]["name"] == "Item 1"
2726
assert result[1]["name"] == "Item 2"
@@ -32,7 +31,7 @@ async def test_get_items_fail(self, async_client):
3231
response = await async_client.simulate_get("/items-fail")
3332

3433
assert response.status_code == 500
35-
result = json.loads(response.text)
34+
result = from_json(response.text)
3635
assert result["error"]["message"] == "TEST ERROR"
3736

3837
@pytest.mark.asyncio
@@ -41,7 +40,7 @@ async def test_get_item(self, async_client):
4140
response = await async_client.simulate_get("/items/1")
4241

4342
assert response.status_code == 200
44-
result = json.loads(response.text)
43+
result = from_json(response.text)
4544
assert result["id"] == 1
4645
assert result["name"] == "Item 1"
4746
assert result["description"] == "Description 1"
@@ -52,7 +51,7 @@ async def test_get_item_bad_request(self, async_client):
5251
response = await async_client.simulate_get("/items/abc")
5352

5453
assert response.status_code == 400
55-
result = json.loads(response.text)
54+
result = from_json(response.text)
5655
assert result["error"]["message"] == (
5756
"Error parsing parameter 'item_id'. Must be a valid int"
5857
)
@@ -70,12 +69,12 @@ async def test_create_item(self, async_client):
7069
new_item = {"name": "New Item", "description": "New Description"}
7170
response = await async_client.simulate_post(
7271
"/items",
73-
body=json.dumps(new_item),
72+
body=to_json(new_item).decode("utf-8"),
7473
headers={"Content-Type": "application/json"},
7574
)
7675

7776
assert response.status_code == 201
78-
result = json.loads(response.text)
77+
result = from_json(response.text)
7978
assert result["id"] == 3
8079
assert result["name"] == "New Item"
8180
assert result["description"] == "New Description"
@@ -86,12 +85,12 @@ async def test_create_item_incorrect(self, async_client):
8685
new_item = {"name": None, "description": "New Description"}
8786
response = await async_client.simulate_post(
8887
"/items",
89-
body=json.dumps(new_item),
88+
body=to_json(new_item).decode("utf-8"),
9089
headers={"Content-Type": "application/json"},
9190
)
9291

9392
assert response.status_code == 422
94-
result = json.loads(response.text)
93+
result = from_json(response.text)
9594
assert "Validation error for parameter" in result["error"]["message"]
9695

9796
@pytest.mark.asyncio
@@ -104,7 +103,7 @@ async def test_create_item_invalid_json(self, async_client):
104103
)
105104

106105
assert response.status_code == 422
107-
result = json.loads(response.text)
106+
result = from_json(response.text)
108107
assert "Validation error for parameter" in result["error"]["message"]
109108

110109
@pytest.mark.asyncio
@@ -113,12 +112,12 @@ async def test_update_item(self, async_client):
113112
update_data = {"name": "Updated Item", "description": "Updated Description"}
114113
response = await async_client.simulate_put(
115114
"/items/2",
116-
body=json.dumps(update_data),
115+
body=to_json(update_data).decode("utf-8"),
117116
headers={"Content-Type": "application/json"},
118117
)
119118

120119
assert response.status_code == 200
121-
result = json.loads(response.text)
120+
result = from_json(response.text)
122121
assert result["id"] == 2
123122
assert result["name"] == "Updated Item"
124123
assert result["description"] == "Updated Description"
@@ -140,7 +139,7 @@ async def test_openapi_schema_endpoint(self, async_client):
140139
response = await async_client.simulate_get("/openapi.json")
141140

142141
assert response.status_code == 200
143-
schema = json.loads(response.text)
142+
schema = from_json(response.text)
144143
assert schema["info"]["title"] == "Test API"
145144
assert "/items" in schema["paths"]
146145
assert "/items/{item_id}" in schema["paths"]
@@ -169,15 +168,15 @@ async def test_query_parameters_handling(self, async_client):
169168
# Test with a single value parameter
170169
response = await async_client.get("/list-test?param1=single_value")
171170
assert response.status_code == 200
172-
data = json.loads(response.text)
171+
data = from_json(response.text)
173172
assert data["received_param1"] == "single_value"
174173

175174
# Test with a parameter that has multiple values
176175
response = await async_client.get(
177176
"/list-test?param1=first_value&param2=value1&param2=value2"
178177
)
179178
assert response.status_code == 200
180-
data = json.loads(response.text)
179+
data = from_json(response.text)
181180
assert data["received_param1"] == "first_value"
182181
assert isinstance(data["received_param2"], list)
183182
assert data["received_param2"] == ["value1", "value2"]

tests/flask/test_flask_integration.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import json
1+
from pydantic_core import from_json, to_json
22

33

44
class TestFlaskIntegration:
@@ -8,7 +8,7 @@ def test_get_items(self, client):
88
response = client.get("/items")
99

1010
assert response.status_code == 200
11-
result = json.loads(response.text)
11+
result = from_json(response.text)
1212
assert len(result) == 2
1313
assert result[0]["name"] == "Item 1"
1414
assert result[1]["name"] == "Item 2"
@@ -18,15 +18,15 @@ def test_get_items_fail(self, client):
1818
response = client.get("/items-fail")
1919

2020
assert response.status_code == 500
21-
result = json.loads(response.text)
21+
result = from_json(response.text)
2222
assert result["error"]["message"] == "TEST ERROR"
2323

2424
def test_get_item(self, client):
2525
"""Test fetching an item by ID"""
2626
response = client.get("/items/1")
2727

2828
assert response.status_code == 200
29-
result = json.loads(response.text)
29+
result = from_json(response.text)
3030
assert result["id"] == 1
3131
assert result["name"] == "Item 1"
3232
assert result["description"] == "Description 1"
@@ -36,7 +36,7 @@ def test_get_item_bad_request(self, client):
3636
response = client.get("/items/abc")
3737

3838
assert response.status_code == 400
39-
result = json.loads(response.text)
39+
result = from_json(response.text)
4040
assert result["error"]["message"] == (
4141
"Error parsing parameter 'item_id'. Must be a valid int"
4242
)
@@ -52,12 +52,12 @@ def test_create_item(self, client):
5252
new_item = {"name": "New Item", "description": "New Description"}
5353
response = client.post(
5454
"/items",
55-
data=json.dumps(new_item),
55+
data=to_json(new_item).decode("utf-8"),
5656
headers={"Content-Type": "application/json"},
5757
)
5858

5959
assert response.status_code == 201
60-
result = json.loads(response.text)
60+
result = from_json(response.text)
6161
assert result["id"] == 3
6262
assert result["name"] == "New Item"
6363
assert result["description"] == "New Description"
@@ -67,12 +67,12 @@ def test_create_item_incorrect(self, client):
6767
new_item = {"name": None, "description": "New Description"}
6868
response = client.post(
6969
"/items",
70-
data=json.dumps(new_item),
70+
data=to_json(new_item).decode("utf-8"),
7171
headers={"Content-Type": "application/json"},
7272
)
7373

7474
assert response.status_code == 422
75-
result = json.loads(response.text)
75+
result = from_json(response.text)
7676
assert "Validation error for parameter" in result["error"]["message"]
7777

7878
def test_create_item_invalid_json(self, client):
@@ -84,20 +84,20 @@ def test_create_item_invalid_json(self, client):
8484
)
8585

8686
assert response.status_code == 422
87-
result = json.loads(response.text)
87+
result = from_json(response.text)
8888
assert "Validation error for parameter" in result["error"]["message"]
8989

9090
def test_update_item(self, client):
9191
"""Test updating an item"""
9292
update_data = {"name": "Updated Item", "description": "Updated Description"}
9393
response = client.put(
9494
"/items/2",
95-
data=json.dumps(update_data),
95+
data=to_json(update_data).decode("utf-8"),
9696
headers={"Content-Type": "application/json"},
9797
)
9898

9999
assert response.status_code == 200
100-
result = json.loads(response.text)
100+
result = from_json(response.text)
101101
assert result["id"] == 2
102102
assert result["name"] == "Updated Item"
103103
assert result["description"] == "Updated Description"
@@ -117,7 +117,7 @@ def test_openapi_schema_endpoint(self, client):
117117
response = client.get("/openapi.json")
118118

119119
assert response.status_code == 200
120-
schema = json.loads(response.text)
120+
schema = from_json(response.text)
121121
assert schema["info"]["title"] == "Test API"
122122
assert "/items" in schema["paths"]
123123
assert "/items/{item_id}" in schema["paths"]

0 commit comments

Comments
 (0)