Repository: tiangolo/fastapi

Issue:
Add support for Pydantic's dataclasses
**Describe the question**
I am using a  pydantic.dataclasses.dataclass as a response_model of my fast api route.


```
from fastapi import FastAPI
from pydantic.dataclasses import dataclass


@dataclass
class Foo:
    bar: int

app = FastAPI()

@app.get('/foo', response_model=Foo)
def get_bar() -> Foo:
    return Foo(bar=1)
```

**To Reproduce**
Steps to reproduce the behavior:
1. Create a file **app.py** with the above code.
2. Start the app: `uvicorn app:app`
3. Open the browser and go to **localhost:8000/foo**
4. See error: 
```
pydantic.error_wrappers.ValidationError: 1 validation error
response
  __init__() got an unexpected keyword argument '__initialised__' (type=type_error)
```

**Expected behavior**
No validation error and serialized json does not contain any keys `__initialised__`.


**Environment:**
 - OS: Linux
 - FastAPI Version 0.25.0
- Python version 3.7.3



Discussion:
**Possible solution**

Add the following to jsonable_encoder in fastapi.encoders:

```
from dataclasses import is_dataclass, asdict

def jsonable_encoder(
    obj: Any,
    include: Set[str] = None,
    exclude: Set[str] = set(),
    by_alias: bool = True,
    skip_defaults: bool = False,
    include_none: bool = True,
    custom_encoder: dict = {},
    sqlalchemy_safe: bool = True,
) -> Any:
...
    if is_dataclass(obj):
        return asdict(obj)
```
Thank you. Pydantic's dataclasses are not supported yet. They probably will be at some point.
Any updates on this?
Using pydantic BaseModel vs dataclass causes tremendous slowdown as well in my codebase.
@manojlds which one is slower?
👍 for this.  I have a backend code base that heavily relies on pydantic dataclasses.

Just a thought, the way pydantic dataclasses are handled is by putting the `BaseModel` subclass at `cls.__pydantic_model__`, e.g.:

```python
from pydantic.dataclasses import dataclass
from pydantic import BaseModel

@dataclass
class Foobar:
    pass

assert issubclass(Foobar.__pydantic_model__, BaseModel)
```

Could this be solved with a simple:
```python
if isdataclass(cls) and hasattr(cls, "__pydantic_model__"):
    cls = cls.__pydantic_model__
```
Somewhere?
Maybe part of https://github.com/tiangolo/fastapi/blob/master/fastapi/utils.py#L53 ?

Resolving patch:
diff --git a/fastapi/utils.py b/fastapi/utils.py
--- a/fastapi/utils.py
+++ b/fastapi/utils.py
@@ -1,4 +1,5 @@
 import re
+from dataclasses import is_dataclass
 from typing import Any, Dict, List, Sequence, Set, Type, cast
 
 from fastapi import routing
@@ -52,6 +53,8 @@ def get_path_param_names(path: str) -> Set[str]:
 
 def create_cloned_field(field: Field) -> Field:
     original_type = field.type_
+    if is_dataclass(original_type) and hasattr(original_type, "__pydantic_model__"):
+        original_type = original_type.__pydantic_model__  # type: ignore
     use_type = original_type
     if lenient_issubclass(original_type, BaseModel):
         original_type = cast(Type[BaseModel], original_type)


Test patch:
diff --git a/tests/test_serialize_response.py b/tests/test_serialize_response.py
--- a/tests/test_serialize_response.py
+++ b/tests/test_serialize_response.py
@@ -1,8 +1,7 @@
 from typing import List
 
-import pytest
 from fastapi import FastAPI
-from pydantic import BaseModel, ValidationError
+from pydantic import BaseModel
 from starlette.testclient import TestClient
 
 app = FastAPI()
@@ -14,38 +13,45 @@ class Item(BaseModel):
     owner_ids: List[int] = None
 
 
-@app.get("/items/invalid", response_model=Item)
-def get_invalid():
-    return {"name": "invalid", "price": "foo"}
+@app.get("/items/valid", response_model=Item)
+def get_valid():
+    return {"name": "valid", "price": 1.0}
 
 
-@app.get("/items/innerinvalid", response_model=Item)
-def get_innerinvalid():
-    return {"name": "double invalid", "price": "foo", "owner_ids": ["foo", "bar"]}
+@app.get("/items/coerce", response_model=Item)
+def get_coerce():
+    return {"name": "coerce", "price": "1.0"}
 
 
-@app.get("/items/invalidlist", response_model=List[Item])
-def get_invalidlist():
+@app.get("/items/validlist", response_model=List[Item])
+def get_validlist():
     return [
         {"name": "foo"},
-        {"name": "bar", "price": "bar"},
-        {"name": "baz", "price": "baz"},
+        {"name": "bar", "price": 1.0},
+        {"name": "baz", "price": 2.0, "owner_ids": [1, 2, 3]},
     ]
 
 
 client = TestClient(app)
 
 
-def test_invalid():
-    with pytest.raises(ValidationError):
-        client.get("/items/invalid")
+def test_valid():
+    response = client.get("/items/valid")
+    response.raise_for_status()
+    assert response.json() == {"name": "valid", "price": 1.0, "owner_ids": None}
 
 
-def test_double_invalid():
-    with pytest.raises(ValidationError):
-        client.get("/items/innerinvalid")
+def test_coerce():
+    response = client.get("/items/coerce")
+    response.raise_for_status()
+    assert response.json() == {"name": "coerce", "price": 1.0, "owner_ids": None}
 
 
-def test_invalid_list():
-    with pytest.raises(ValidationError):
-        client.get("/items/invalidlist")
+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]},
+    ]
diff --git a/tests/test_serialize_response_dataclass.py b/tests/test_serialize_response_dataclass.py
new file mode 100644
--- /dev/null
+++ b/tests/test_serialize_response_dataclass.py
@@ -0,0 +1,58 @@
+from typing import List
+
+from fastapi import FastAPI
+from pydantic.dataclasses import dataclass
+from starlette.testclient import TestClient
+
+app = FastAPI()
+
+
+@dataclass
+class Item:
+    name: str
+    price: float = None
+    owner_ids: List[int] = None
+
+
+@app.get("/items/valid", response_model=Item)
+def get_valid():
+    return {"name": "valid", "price": 1.0}
+
+
+@app.get("/items/coerce", response_model=Item)
+def get_coerce():
+    return {"name": "coerce", "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]},
+    ]
+
+
+client = TestClient(app)
+
+
+def test_valid():
+    response = client.get("/items/valid")
+    response.raise_for_status()
+    assert response.json() == {"name": "valid", "price": 1.0, "owner_ids": None}
+
+
+def test_coerce():
+    response = client.get("/items/coerce")
+    response.raise_for_status()
+    assert response.json() == {"name": "coerce", "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]},
+    ]
diff --git a/tests/test_validate_response.py b/tests/test_validate_response.py
new file mode 100644
--- /dev/null
+++ b/tests/test_validate_response.py
@@ -0,0 +1,51 @@
+from typing import List
+
+import pytest
+from fastapi import FastAPI
+from pydantic import BaseModel, ValidationError
+from starlette.testclient import TestClient
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+    name: str
+    price: float = None
+    owner_ids: List[int] = None
+
+
+@app.get("/items/invalid", response_model=Item)
+def get_invalid():
+    return {"name": "invalid", "price": "foo"}
+
+
+@app.get("/items/innerinvalid", response_model=Item)
+def get_innerinvalid():
+    return {"name": "double invalid", "price": "foo", "owner_ids": ["foo", "bar"]}
+
+
+@app.get("/items/invalidlist", response_model=List[Item])
+def get_invalidlist():
+    return [
+        {"name": "foo"},
+        {"name": "bar", "price": "bar"},
+        {"name": "baz", "price": "baz"},
+    ]
+
+
+client = TestClient(app)
+
+
+def test_invalid():
+    with pytest.raises(ValidationError):
+        client.get("/items/invalid")
+
+
+def test_double_invalid():
+    with pytest.raises(ValidationError):
+        client.get("/items/innerinvalid")
+
+
+def test_invalid_list():
+    with pytest.raises(ValidationError):
+        client.get("/items/invalidlist")
diff --git a/tests/test_validate_response_dataclass.py b/tests/test_validate_response_dataclass.py
new file mode 100644
--- /dev/null
+++ b/tests/test_validate_response_dataclass.py
@@ -0,0 +1,53 @@
+from typing import List
+
+import pytest
+from fastapi import FastAPI
+from pydantic import ValidationError
+from pydantic.dataclasses import dataclass
+from starlette.testclient import TestClient
+
+app = FastAPI()
+
+
+@dataclass
+class Item:
+    name: str
+    price: float = None
+    owner_ids: List[int] = None
+
+
+@app.get("/items/invalid", response_model=Item)
+def get_invalid():
+    return {"name": "invalid", "price": "foo"}
+
+
+@app.get("/items/innerinvalid", response_model=Item)
+def get_innerinvalid():
+    return {"name": "double invalid", "price": "foo", "owner_ids": ["foo", "bar"]}
+
+
+@app.get("/items/invalidlist", response_model=List[Item])
+def get_invalidlist():
+    return [
+        {"name": "foo"},
+        {"name": "bar", "price": "bar"},
+        {"name": "baz", "price": "baz"},
+    ]
+
+
+client = TestClient(app)
+
+
+def test_invalid():
+    with pytest.raises(ValidationError):
+        client.get("/items/invalid")
+
+
+def test_double_invalid():
+    with pytest.raises(ValidationError):
+        client.get("/items/innerinvalid")
+
+
+def test_invalid_list():
+    with pytest.raises(ValidationError):
+        client.get("/items/invalidlist")