-
-
Notifications
You must be signed in to change notification settings - Fork 6.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ Add support for
dataclasses
in request bodies and response_model
(
- Loading branch information
Showing
10 changed files
with
562 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
# Using Dataclasses | ||
|
||
FastAPI is built on top of **Pydantic**, and I have been showing you how to use Pydantic models to declare requests and responses. | ||
|
||
But FastAPI also supports using <a href="https://docs.python.org/3/library/dataclasses.html" class="external-link" target="_blank">`dataclasses`</a> the same way: | ||
|
||
```Python hl_lines="1 7-12 19-20" | ||
{!../../../docs_src/dataclasses/tutorial001.py!} | ||
``` | ||
|
||
This is still thanks to **Pydantic**, as it has <a href="https://pydantic-docs.helpmanual.io/usage/dataclasses/#use-of-stdlib-dataclasses-with-basemodel" class="external-link" target="_blank">internal support for `dataclasses`</a>. | ||
|
||
So, even with the code above that doesn't use Pydantic explicitly, FastAPI is using Pydantic to convert those standard dataclasses to Pydantic's own flavor of dataclasses. | ||
|
||
And of course, it supports the same: | ||
|
||
* data validation | ||
* data serialization | ||
* data documentation, etc. | ||
|
||
This works the same way as with Pydantic models. And it is actually achieved in the same way underneath, using Pydantic. | ||
|
||
!!! info | ||
Have in mind that dataclasses can't do everything Pydantic models can do. | ||
|
||
So, you might still need to use Pydantic models. | ||
|
||
But if you have a bunch of dataclasses laying around, this is a nice trick to use them to power a web API using FastAPI. 🤓 | ||
|
||
## Dataclasses in `response_model` | ||
|
||
You can also use `dataclasses` in the `response_model` parameter: | ||
|
||
```Python hl_lines="1 7-13 19" | ||
{!../../../docs_src/dataclasses/tutorial002.py!} | ||
``` | ||
|
||
The dataclass will be automatically converted to a Pydantic dataclass. | ||
|
||
This way, its schema will show up in the API docs user interface: | ||
|
||
<img src="/img/tutorial/dataclasses/image01.png"> | ||
|
||
## Dataclasses in Nested Data Structures | ||
|
||
You can also combine `dataclasses` with other type annotations to make nested data structures. | ||
|
||
In some cases, you might still have to use Pydantic's version of `dataclasses`. For example, if you have errors with the automatically generated API documentation. | ||
|
||
In that case, you can simply swap the standard `dataclasses` with `pydantic.dataclasses`, which is a drop-in replacement: | ||
|
||
```{ .python .annotate hl_lines="1 5 8-11 14-17 23-25 28" } | ||
{!../../../docs_src/dataclasses/tutorial003.py!} | ||
``` | ||
|
||
1. We still import `field` from standard `dataclasses`. | ||
|
||
2. `pydantic.dataclasses` is a drop-in replacement for `dataclasses`. | ||
|
||
3. The `Author` dataclass includes a list of `Item` dataclasses. | ||
|
||
4. The `Author` dataclass is used as the `response_model` parameter. | ||
|
||
5. You can use other standard type annotations with dataclasses as the request body. | ||
|
||
In this case, it's a list of `Item` dataclasses. | ||
|
||
6. Here we are returning a dictionary that contains `items` which is a list of dataclasses. | ||
|
||
FastAPI is still capable of <abbr title="converting the data to a format that can be transmitted">serializing</abbr> the data to JSON. | ||
|
||
7. Here the `response_model` is using a type annotation of a list of `Author` dataclasses. | ||
|
||
Again, you can combine `dataclasses` with standard type annotations. | ||
|
||
8. Notice that this *path operation function* uses regular `def` instead of `async def`. | ||
|
||
As always, in FastAPI you can combine `def` and `async def` as needed. | ||
|
||
If you need a refresher about when to use which, check out the section _"In a hurry?"_ in the docs about <a href="https://fastapi.tiangolo.com/async/#in-a-hurry" target="_blank" class="internal-link">`async` and `await`</a>. | ||
|
||
9. This *path operation function* is not returning dataclasses (although it could), but a list of dictionaries with internal data. | ||
|
||
FastAPI will use the `response_model` parameter (that includes dataclasses) to convert the response. | ||
|
||
You can combine `dataclasses` with other type annotations in many different combinations to form complex data structures. | ||
|
||
Check the in-code annotation tips above to see more specific details. | ||
|
||
## Learn More | ||
|
||
You can also combine `dataclasses` with other Pydantic models, inherit from them, include them in your own models, etc. | ||
|
||
To learn more, check the <a href="https://pydantic-docs.helpmanual.io/usage/dataclasses/" class="external-link" target="_blank">Pydantic docs about dataclasses</a>. | ||
|
||
## Version | ||
|
||
This is available since FastAPI version `0.67.0`. 🔖 |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
from dataclasses import dataclass | ||
from typing import Optional | ||
|
||
from fastapi import FastAPI | ||
|
||
|
||
@dataclass | ||
class Item: | ||
name: str | ||
price: float | ||
description: Optional[str] = None | ||
tax: Optional[float] = None | ||
|
||
|
||
app = FastAPI() | ||
|
||
|
||
@app.post("/items/") | ||
async def create_item(item: Item): | ||
return item |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
from dataclasses import dataclass, field | ||
from typing import List, Optional | ||
|
||
from fastapi import FastAPI | ||
|
||
|
||
@dataclass | ||
class Item: | ||
name: str | ||
price: float | ||
tags: List[str] = field(default_factory=list) | ||
description: Optional[str] = None | ||
tax: Optional[float] = None | ||
|
||
|
||
app = FastAPI() | ||
|
||
|
||
@app.get("/items/next", response_model=Item) | ||
async def read_next_item(): | ||
return { | ||
"name": "Island In The Moon", | ||
"price": 12.99, | ||
"description": "A place to be be playin' and havin' fun", | ||
"tags": ["breater"], | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
from dataclasses import field # (1) | ||
from typing import List, Optional | ||
|
||
from fastapi import FastAPI | ||
from pydantic.dataclasses import dataclass # (2) | ||
|
||
|
||
@dataclass | ||
class Item: | ||
name: str | ||
description: Optional[str] = None | ||
|
||
|
||
@dataclass | ||
class Author: | ||
name: str | ||
items: List[Item] = field(default_factory=list) # (3) | ||
|
||
|
||
app = FastAPI() | ||
|
||
|
||
@app.post("/authors/{author_id}/items/", response_model=Author) # (4) | ||
async def create_author_items(author_id: str, items: List[Item]): # (5) | ||
return {"name": author_id, "items": items} # (6) | ||
|
||
|
||
@app.get("/authors/", response_model=List[Author]) # (7) | ||
def get_authors(): # (8) | ||
return [ # (9) | ||
{ | ||
"name": "Breaters", | ||
"items": [ | ||
{ | ||
"name": "Island In The Moon", | ||
"description": "A place to be be playin' and havin' fun", | ||
}, | ||
{"name": "Holy Buddies"}, | ||
], | ||
}, | ||
{ | ||
"name": "System of an Up", | ||
"items": [ | ||
{ | ||
"name": "Salt", | ||
"description": "The kombucha mushroom people's favorite", | ||
}, | ||
{"name": "Pad Thai"}, | ||
{ | ||
"name": "Lonely Night", | ||
"description": "The mostests lonliest nightiest of allest", | ||
}, | ||
], | ||
}, | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
113 changes: 113 additions & 0 deletions
113
tests/test_tutorial/test_dataclasses/test_tutorial001.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
from fastapi.testclient import TestClient | ||
|
||
from docs_src.dataclasses.tutorial001 import app | ||
|
||
client = TestClient(app) | ||
|
||
openapi_schema = { | ||
"openapi": "3.0.2", | ||
"info": {"title": "FastAPI", "version": "0.1.0"}, | ||
"paths": { | ||
"/items/": { | ||
"post": { | ||
"summary": "Create Item", | ||
"operationId": "create_item_items__post", | ||
"requestBody": { | ||
"content": { | ||
"application/json": { | ||
"schema": {"$ref": "#/components/schemas/Item"} | ||
} | ||
}, | ||
"required": True, | ||
}, | ||
"responses": { | ||
"200": { | ||
"description": "Successful Response", | ||
"content": {"application/json": {"schema": {}}}, | ||
}, | ||
"422": { | ||
"description": "Validation Error", | ||
"content": { | ||
"application/json": { | ||
"schema": { | ||
"$ref": "#/components/schemas/HTTPValidationError" | ||
} | ||
} | ||
}, | ||
}, | ||
}, | ||
} | ||
} | ||
}, | ||
"components": { | ||
"schemas": { | ||
"HTTPValidationError": { | ||
"title": "HTTPValidationError", | ||
"type": "object", | ||
"properties": { | ||
"detail": { | ||
"title": "Detail", | ||
"type": "array", | ||
"items": {"$ref": "#/components/schemas/ValidationError"}, | ||
} | ||
}, | ||
}, | ||
"Item": { | ||
"title": "Item", | ||
"required": ["name", "price"], | ||
"type": "object", | ||
"properties": { | ||
"name": {"title": "Name", "type": "string"}, | ||
"price": {"title": "Price", "type": "number"}, | ||
"description": {"title": "Description", "type": "string"}, | ||
"tax": {"title": "Tax", "type": "number"}, | ||
}, | ||
}, | ||
"ValidationError": { | ||
"title": "ValidationError", | ||
"required": ["loc", "msg", "type"], | ||
"type": "object", | ||
"properties": { | ||
"loc": { | ||
"title": "Location", | ||
"type": "array", | ||
"items": {"type": "string"}, | ||
}, | ||
"msg": {"title": "Message", "type": "string"}, | ||
"type": {"title": "Error Type", "type": "string"}, | ||
}, | ||
}, | ||
} | ||
}, | ||
} | ||
|
||
|
||
def test_openapi_schema(): | ||
response = client.get("/openapi.json") | ||
assert response.status_code == 200 | ||
assert response.json() == openapi_schema | ||
|
||
|
||
def test_post_item(): | ||
response = client.post("/items/", json={"name": "Foo", "price": 3}) | ||
assert response.status_code == 200 | ||
assert response.json() == { | ||
"name": "Foo", | ||
"price": 3, | ||
"description": None, | ||
"tax": None, | ||
} | ||
|
||
|
||
def test_post_invalid_item(): | ||
response = client.post("/items/", json={"name": "Foo", "price": "invalid price"}) | ||
assert response.status_code == 422 | ||
assert response.json() == { | ||
"detail": [ | ||
{ | ||
"loc": ["body", "price"], | ||
"msg": "value is not a valid float", | ||
"type": "type_error.float", | ||
} | ||
] | ||
} |
Oops, something went wrong.