Skip to content

Commit 4a3cf5a

Browse files
authored
fix: json serialisation (#1301)
1 parent 27a97b0 commit 4a3cf5a

4 files changed

Lines changed: 125 additions & 7 deletions

File tree

integration_tests/base_routes.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,60 @@ async def async_json_get():
376376
return jsonify({"async json get": "json"})
377377

378378

379+
# JSON List (auto-serialized without explicit jsonify)
380+
381+
382+
@app.get("/sync/json/list")
383+
def sync_json_list_get():
384+
return [
385+
{"id": 1, "title": "First Post", "published": True},
386+
{"id": 2, "title": "Draft Post", "published": False},
387+
{"id": 3, "title": "Latest Post", "published": True},
388+
]
389+
390+
391+
@app.get("/async/json/list")
392+
async def async_json_list_get():
393+
return [
394+
{"id": 1, "title": "First Post", "published": True},
395+
{"id": 2, "title": "Draft Post", "published": False},
396+
{"id": 3, "title": "Latest Post", "published": True},
397+
]
398+
399+
400+
@app.get("/sync/json/list/empty")
401+
def sync_json_list_empty_get():
402+
return []
403+
404+
405+
@app.get("/async/json/list/empty")
406+
async def async_json_list_empty_get():
407+
return []
408+
409+
410+
@app.get("/sync/json/list/primitives")
411+
def sync_json_list_primitives_get():
412+
return [1, 2, 3, "four", True, None]
413+
414+
415+
@app.get("/async/json/list/primitives")
416+
async def async_json_list_primitives_get():
417+
return [1, 2, 3, "four", True, None]
418+
419+
420+
# JSON Dict (auto-serialized without explicit jsonify)
421+
422+
423+
@app.get("/sync/json/dict")
424+
def sync_json_dict_get():
425+
return {"message": "sync dict", "count": 42, "active": True}
426+
427+
428+
@app.get("/async/json/dict")
429+
async def async_json_dict_get():
430+
return {"message": "async dict", "count": 42, "active": True}
431+
432+
379433
@app.get("/sync/json/const", const=True)
380434
def sync_json_const_get():
381435
return jsonify({"sync json const get": "json"})

integration_tests/test_json_types.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22

3-
from integration_tests.helpers.http_methods_helpers import json_post
3+
from integration_tests.helpers.http_methods_helpers import get, json_post
44

55

66
@pytest.mark.parametrize("function_type", ["sync", "async"])
@@ -117,3 +117,65 @@ def test_json_mixed_types_preserved(function_type: str, session):
117117
assert result["field_name"]["type"] == "str"
118118
assert result["field_value"]["value"] == "111000111"
119119
assert result["field_value"]["type"] == "str"
120+
121+
122+
# ===== JSON List Serialization Tests (Issue #1300) =====
123+
124+
125+
@pytest.mark.parametrize("function_type", ["sync", "async"])
126+
def test_json_list_response_serialization(function_type: str, session):
127+
"""Test that returning a list from a handler is properly serialized as JSON"""
128+
res = get(f"/{function_type}/json/list")
129+
130+
# Check content type is application/json
131+
assert res.headers["content-type"] == "application/json"
132+
133+
# Check that response is valid JSON (not Python str representation)
134+
result = res.json()
135+
assert isinstance(result, list)
136+
assert len(result) == 3
137+
138+
# Verify the data structure and types are correct
139+
assert result[0] == {"id": 1, "title": "First Post", "published": True}
140+
assert result[1] == {"id": 2, "title": "Draft Post", "published": False}
141+
assert result[2] == {"id": 3, "title": "Latest Post", "published": True}
142+
143+
# Verify booleans are proper JSON booleans (true/false), not Python (True/False)
144+
# This is implicitly tested by res.json() succeeding, but let's verify the raw response too
145+
assert "true" in res.text.lower()
146+
assert "false" in res.text.lower()
147+
assert "True" not in res.text # Python boolean should not appear
148+
assert "False" not in res.text
149+
150+
151+
@pytest.mark.parametrize("function_type", ["sync", "async"])
152+
def test_json_empty_list_response_serialization(function_type: str, session):
153+
"""Test that returning an empty list is properly serialized as JSON"""
154+
res = get(f"/{function_type}/json/list/empty")
155+
156+
assert res.headers["content-type"] == "application/json"
157+
result = res.json()
158+
assert result == []
159+
assert res.text == "[]"
160+
161+
162+
@pytest.mark.parametrize("function_type", ["sync", "async"])
163+
def test_json_list_primitives_response_serialization(function_type: str, session):
164+
"""Test that a list of primitives is properly serialized as JSON"""
165+
res = get(f"/{function_type}/json/list/primitives")
166+
167+
assert res.headers["content-type"] == "application/json"
168+
result = res.json()
169+
assert result == [1, 2, 3, "four", True, None]
170+
171+
172+
@pytest.mark.parametrize("function_type", ["sync", "async"])
173+
def test_json_dict_response_auto_serialization(function_type: str, session):
174+
"""Test that returning a dict from a handler is properly auto-serialized as JSON"""
175+
res = get(f"/{function_type}/json/dict")
176+
177+
assert res.headers["content-type"] == "application/json"
178+
result = res.json()
179+
assert result["message"] == f"{function_type} dict"
180+
assert result["count"] == 42
181+
assert result["active"] is True

robyn/jsonify.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1+
from typing import Any, Dict, List, Union
2+
13
import orjson
24

35

4-
def jsonify(input_dict: dict) -> str:
6+
def jsonify(data: Union[Dict[str, Any], List[Any]]) -> str:
57
"""
6-
This function serializes input dict to a json string
8+
This function serializes input data to a json string
79
810
Attributes:
9-
input_dict dict: response of the function
11+
data: dict or list to serialize as JSON response
1012
"""
11-
output_binary = orjson.dumps(input_dict)
13+
output_binary = orjson.dumps(data)
1214
output_str = output_binary.decode("utf-8")
1315
return output_str

robyn/router.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,15 @@ def _format_tuple_response(self, res: tuple) -> Response:
7272

7373
def _format_response(
7474
self,
75-
res: Union[Dict, Response, StreamingResponse, bytes, tuple, str],
75+
res: Union[Dict, List, Response, StreamingResponse, bytes, tuple, str],
7676
) -> Union[Response, StreamingResponse]:
7777
if isinstance(res, Response):
7878
return res
7979

8080
if isinstance(res, StreamingResponse):
8181
return res
8282

83-
if isinstance(res, dict):
83+
if isinstance(res, (dict, list)):
8484
return Response(
8585
status_code=status_codes.HTTP_200_OK,
8686
headers=Headers({"Content-Type": "application/json"}),

0 commit comments

Comments
 (0)