From cd268780ed73b4bd5369201e64a0ef8530b3e267 Mon Sep 17 00:00:00 2001 From: Luis Riegger Date: Mon, 26 Jul 2021 16:40:55 +0200 Subject: [PATCH 1/4] Add datetime field to dataclass response -> failing for "no response model" cases. --- tests/test_serialize_response_dataclass.py | 114 ++++++++++++++++----- 1 file changed, 87 insertions(+), 27 deletions(-) diff --git a/tests/test_serialize_response_dataclass.py b/tests/test_serialize_response_dataclass.py index e520338ec68f0..bf6d20655104a 100644 --- a/tests/test_serialize_response_dataclass.py +++ b/tests/test_serialize_response_dataclass.py @@ -1,5 +1,5 @@ from typing import List, Optional - +from datetime import datetime from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic.dataclasses import dataclass @@ -10,54 +10,64 @@ @dataclass class Item: name: str + date: datetime price: Optional[float] = None owner_ids: Optional[List[int]] = None @app.get("/items/valid", response_model=Item) def get_valid(): - return {"name": "valid", "price": 1.0} + return {"name": "valid", "date": datetime(2021, 7, 26), "price": 1.0} @app.get("/items/object", response_model=Item) def get_object(): - return Item(name="object", price=1.0, owner_ids=[1, 2, 3]) + return Item( + name="object", date=datetime(2021, 7, 26), price=1.0, owner_ids=[1, 2, 3] + ) @app.get("/items/coerce", response_model=Item) def get_coerce(): - return {"name": "coerce", "price": "1.0"} + return {"name": "coerce", "date": datetime(2021, 7, 26).isoformat(), "price": "1.0"} @app.get("/items/validlist", response_model=List[Item]) def get_validlist(): return [ - {"name": "foo"}, - {"name": "bar", "price": 1.0}, - {"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]}, + {"name": "foo", "date": datetime(2021, 7, 26)}, + {"name": "bar", "date": datetime(2021, 7, 26), "price": 1.0}, + { + "name": "baz", + "date": datetime(2021, 7, 26), + "price": 2.0, + "owner_ids": [1, 2, 3], + }, ] @app.get("/items/objectlist", response_model=List[Item]) def get_objectlist(): return [ - Item(name="foo"), - Item(name="bar", price=1.0), - Item(name="baz", price=2.0, owner_ids=[1, 2, 3]), + Item(name="foo", date=datetime(2021, 7, 26)), + Item(name="bar", date=datetime(2021, 7, 26), price=1.0), + Item(name="baz", date=datetime(2021, 7, 26), price=2.0, owner_ids=[1, 2, 3]), ] @app.get("/items/no-response-model/object") def get_no_response_model_object(): - return Item(name="object", price=1.0, owner_ids=[1, 2, 3]) + return Item( + name="object", date=datetime(2021, 7, 26), price=1.0, owner_ids=[1, 2, 3] + ) @app.get("/items/no-response-model/objectlist") def get_no_response_model_objectlist(): return [ - Item(name="foo"), - Item(name="bar", price=1.0), - Item(name="baz", price=2.0, owner_ids=[1, 2, 3]), + Item(name="foo", date=datetime(2021, 7, 26)), + Item(name="bar", date=datetime(2021, 7, 26), price=1.0), + Item(name="baz", date=datetime(2021, 7, 26), price=2.0, owner_ids=[1, 2, 3]), ] @@ -67,28 +77,58 @@ def get_no_response_model_objectlist(): def test_valid(): response = client.get("/items/valid") response.raise_for_status() - assert response.json() == {"name": "valid", "price": 1.0, "owner_ids": None} + assert response.json() == { + "name": "valid", + "date": datetime(2021, 7, 26).isoformat(), + "price": 1.0, + "owner_ids": None, + } def test_object(): response = client.get("/items/object") response.raise_for_status() - assert response.json() == {"name": "object", "price": 1.0, "owner_ids": [1, 2, 3]} + assert response.json() == { + "name": "object", + "date": datetime(2021, 7, 26).isoformat(), + "price": 1.0, + "owner_ids": [1, 2, 3], + } def test_coerce(): response = client.get("/items/coerce") response.raise_for_status() - assert response.json() == {"name": "coerce", "price": 1.0, "owner_ids": None} + assert response.json() == { + "name": "coerce", + "date": datetime(2021, 7, 26).isoformat(), + "price": 1.0, + "owner_ids": None, + } def test_validlist(): response = client.get("/items/validlist") response.raise_for_status() assert response.json() == [ - {"name": "foo", "price": None, "owner_ids": None}, - {"name": "bar", "price": 1.0, "owner_ids": None}, - {"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]}, + { + "name": "foo", + "date": datetime(2021, 7, 26).isoformat(), + "price": None, + "owner_ids": None, + }, + { + "name": "bar", + "date": datetime(2021, 7, 26).isoformat(), + "price": 1.0, + "owner_ids": None, + }, + { + "name": "baz", + "date": datetime(2021, 7, 26).isoformat(), + "price": 2.0, + "owner_ids": [1, 2, 3], + }, ] @@ -96,23 +136,43 @@ def test_objectlist(): response = client.get("/items/objectlist") response.raise_for_status() assert response.json() == [ - {"name": "foo", "price": None, "owner_ids": None}, - {"name": "bar", "price": 1.0, "owner_ids": None}, - {"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]}, + { + "name": "foo", + "date": datetime(2021, 7, 26).isoformat(), + "price": None, + "owner_ids": None, + }, + { + "name": "bar", + "date": datetime(2021, 7, 26).isoformat(), + "price": 1.0, + "owner_ids": None, + }, + { + "name": "baz", + "date": datetime(2021, 7, 26).isoformat(), + "price": 2.0, + "owner_ids": [1, 2, 3], + }, ] def test_no_response_model_object(): response = client.get("/items/no-response-model/object") response.raise_for_status() - assert response.json() == {"name": "object", "price": 1.0, "owner_ids": [1, 2, 3]} + assert response.json() == { + "name": "object", + "date": datetime(2021, 7, 26).isoformat(), + "price": 1.0, + "owner_ids": [1, 2, 3], + } def test_no_response_model_objectlist(): response = client.get("/items/no-response-model/objectlist") response.raise_for_status() assert response.json() == [ - {"name": "foo", "price": None, "owner_ids": None}, - {"name": "bar", "price": 1.0, "owner_ids": None}, - {"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]}, + {"name": "foo", "date": datetime(2021, 7, 26).isoformat(), "price": None, "owner_ids": None}, + {"name": "bar", "date": datetime(2021, 7, 26).isoformat(), "price": 1.0, "owner_ids": None}, + {"name": "baz", "date": datetime(2021, 7, 26).isoformat(), "price": 2.0, "owner_ids": [1, 2, 3]}, ] From 121b3f1af5a0386c0ebcd50783180b41ede644e1 Mon Sep 17 00:00:00 2001 From: Luis Riegger Date: Mon, 26 Jul 2021 16:51:00 +0200 Subject: [PATCH 2/4] Add call to `jsonable_encoder(dict)` after converting dataclass to dict in `jsonable_encoder`. --- fastapi/encoders.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/fastapi/encoders.py b/fastapi/encoders.py index 3f599c9faa045..1b141673e9659 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -63,7 +63,14 @@ def jsonable_encoder( sqlalchemy_safe=sqlalchemy_safe, ) if dataclasses.is_dataclass(obj): - return dataclasses.asdict(obj) + obj_dict = dataclasses.asdict(obj) + return jsonable_encoder( + obj_dict, + exclude_none=exclude_none, + exclude_defaults=exclude_defaults, + custom_encoder=custom_encoder, + sqlalchemy_safe=sqlalchemy_safe, + ) if isinstance(obj, Enum): return obj.value if isinstance(obj, PurePath): From b79b58da9a72f49e3c5ac76f52d31945b065f415 Mon Sep 17 00:00:00 2001 From: Luis Riegger Date: Mon, 2 Aug 2021 21:13:24 +0200 Subject: [PATCH 3/4] Use stdlib dataclass instead of pydantic dataclass --- tests/test_serialize_response_dataclass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_serialize_response_dataclass.py b/tests/test_serialize_response_dataclass.py index bf6d20655104a..8583fac1aadd5 100644 --- a/tests/test_serialize_response_dataclass.py +++ b/tests/test_serialize_response_dataclass.py @@ -2,7 +2,7 @@ from datetime import datetime from fastapi import FastAPI from fastapi.testclient import TestClient -from pydantic.dataclasses import dataclass +from dataclasses import dataclass app = FastAPI() From e722c7bdefe62e380369dc0b8ec08cb1f0d898a5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 26 Aug 2022 13:51:39 +0000 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20for?= =?UTF-8?q?mat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/encoders.py | 2 +- tests/test_serialize_response_dataclass.py | 26 +++++++++++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/fastapi/encoders.py b/fastapi/encoders.py index 369defe0f2267..f64e4b86ea3b4 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -71,7 +71,7 @@ def jsonable_encoder( sqlalchemy_safe=sqlalchemy_safe, ) if dataclasses.is_dataclass(obj): - obj_dict = dataclasses.asdict(obj) + obj_dict = dataclasses.asdict(obj) return jsonable_encoder( obj_dict, exclude_none=exclude_none, diff --git a/tests/test_serialize_response_dataclass.py b/tests/test_serialize_response_dataclass.py index 8583fac1aadd5..1e3bf3b28bcc3 100644 --- a/tests/test_serialize_response_dataclass.py +++ b/tests/test_serialize_response_dataclass.py @@ -1,8 +1,9 @@ -from typing import List, Optional +from dataclasses import dataclass from datetime import datetime +from typing import List, Optional + from fastapi import FastAPI from fastapi.testclient import TestClient -from dataclasses import dataclass app = FastAPI() @@ -172,7 +173,22 @@ def test_no_response_model_objectlist(): response = client.get("/items/no-response-model/objectlist") response.raise_for_status() assert response.json() == [ - {"name": "foo", "date": datetime(2021, 7, 26).isoformat(), "price": None, "owner_ids": None}, - {"name": "bar", "date": datetime(2021, 7, 26).isoformat(), "price": 1.0, "owner_ids": None}, - {"name": "baz", "date": datetime(2021, 7, 26).isoformat(), "price": 2.0, "owner_ids": [1, 2, 3]}, + { + "name": "foo", + "date": datetime(2021, 7, 26).isoformat(), + "price": None, + "owner_ids": None, + }, + { + "name": "bar", + "date": datetime(2021, 7, 26).isoformat(), + "price": 1.0, + "owner_ids": None, + }, + { + "name": "baz", + "date": datetime(2021, 7, 26).isoformat(), + "price": 2.0, + "owner_ids": [1, 2, 3], + }, ]