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

Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Luis R 2022-08-26 15:56:47 +02:00 committed by GitHub
parent de6ccd7754
commit 22bed0008c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 111 additions and 28 deletions

View File

@ -71,7 +71,14 @@ def jsonable_encoder(
sqlalchemy_safe=sqlalchemy_safe, sqlalchemy_safe=sqlalchemy_safe,
) )
if dataclasses.is_dataclass(obj): 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): if isinstance(obj, Enum):
return obj.value return obj.value
if isinstance(obj, PurePath): if isinstance(obj, PurePath):

View File

@ -1,8 +1,9 @@
from dataclasses import dataclass
from datetime import datetime
from typing import List, Optional from typing import List, Optional
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from pydantic.dataclasses import dataclass
app = FastAPI() app = FastAPI()
@ -10,54 +11,64 @@ app = FastAPI()
@dataclass @dataclass
class Item: class Item:
name: str name: str
date: datetime
price: Optional[float] = None price: Optional[float] = None
owner_ids: Optional[List[int]] = None owner_ids: Optional[List[int]] = None
@app.get("/items/valid", response_model=Item) @app.get("/items/valid", response_model=Item)
def get_valid(): 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) @app.get("/items/object", response_model=Item)
def get_object(): 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) @app.get("/items/coerce", response_model=Item)
def get_coerce(): 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]) @app.get("/items/validlist", response_model=List[Item])
def get_validlist(): def get_validlist():
return [ return [
{"name": "foo"}, {"name": "foo", "date": datetime(2021, 7, 26)},
{"name": "bar", "price": 1.0}, {"name": "bar", "date": datetime(2021, 7, 26), "price": 1.0},
{"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]}, {
"name": "baz",
"date": datetime(2021, 7, 26),
"price": 2.0,
"owner_ids": [1, 2, 3],
},
] ]
@app.get("/items/objectlist", response_model=List[Item]) @app.get("/items/objectlist", response_model=List[Item])
def get_objectlist(): def get_objectlist():
return [ return [
Item(name="foo"), Item(name="foo", date=datetime(2021, 7, 26)),
Item(name="bar", price=1.0), Item(name="bar", date=datetime(2021, 7, 26), price=1.0),
Item(name="baz", price=2.0, owner_ids=[1, 2, 3]), Item(name="baz", date=datetime(2021, 7, 26), price=2.0, owner_ids=[1, 2, 3]),
] ]
@app.get("/items/no-response-model/object") @app.get("/items/no-response-model/object")
def get_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") @app.get("/items/no-response-model/objectlist")
def get_no_response_model_objectlist(): def get_no_response_model_objectlist():
return [ return [
Item(name="foo"), Item(name="foo", date=datetime(2021, 7, 26)),
Item(name="bar", price=1.0), Item(name="bar", date=datetime(2021, 7, 26), price=1.0),
Item(name="baz", price=2.0, owner_ids=[1, 2, 3]), Item(name="baz", date=datetime(2021, 7, 26), price=2.0, owner_ids=[1, 2, 3]),
] ]
@ -67,28 +78,58 @@ client = TestClient(app)
def test_valid(): def test_valid():
response = client.get("/items/valid") response = client.get("/items/valid")
response.raise_for_status() 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(): def test_object():
response = client.get("/items/object") response = client.get("/items/object")
response.raise_for_status() 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(): def test_coerce():
response = client.get("/items/coerce") response = client.get("/items/coerce")
response.raise_for_status() 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(): def test_validlist():
response = client.get("/items/validlist") response = client.get("/items/validlist")
response.raise_for_status() response.raise_for_status()
assert response.json() == [ assert response.json() == [
{"name": "foo", "price": None, "owner_ids": None}, {
{"name": "bar", "price": 1.0, "owner_ids": None}, "name": "foo",
{"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]}, "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 +137,58 @@ def test_objectlist():
response = client.get("/items/objectlist") response = client.get("/items/objectlist")
response.raise_for_status() response.raise_for_status()
assert response.json() == [ assert response.json() == [
{"name": "foo", "price": None, "owner_ids": None}, {
{"name": "bar", "price": 1.0, "owner_ids": None}, "name": "foo",
{"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]}, "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(): def test_no_response_model_object():
response = client.get("/items/no-response-model/object") response = client.get("/items/no-response-model/object")
response.raise_for_status() 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(): def test_no_response_model_objectlist():
response = client.get("/items/no-response-model/objectlist") response = client.get("/items/no-response-model/objectlist")
response.raise_for_status() response.raise_for_status()
assert response.json() == [ assert response.json() == [
{"name": "foo", "price": None, "owner_ids": None}, {
{"name": "bar", "price": 1.0, "owner_ids": None}, "name": "foo",
{"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]}, "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],
},
] ]