Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 Fix jsonable_encoder for dataclasses with pydantic-compatible fields #3607

Merged
merged 6 commits into from Aug 26, 2022
9 changes: 8 additions & 1 deletion fastapi/encoders.py
Expand Up @@ -71,7 +71,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):
Expand Down
130 changes: 103 additions & 27 deletions tests/test_serialize_response_dataclass.py
@@ -1,63 +1,74 @@
from dataclasses import dataclass
from datetime import datetime
from typing import List, Optional

from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic.dataclasses import dataclass

app = FastAPI()


@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]),
]


Expand All @@ -67,52 +78,117 @@ 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],
},
]


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],
},
]