Skip to content

Commit

Permalink
✨ Add support for function return type annotations to declare the `re…
Browse files Browse the repository at this point in the history
…sponse_model` (tiangolo#1436)

Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
  • Loading branch information
2 people authored and JonasKs committed Jan 12, 2023
1 parent 62d83b1 commit 2938735
Show file tree
Hide file tree
Showing 18 changed files with 1,368 additions and 58 deletions.
142 changes: 120 additions & 22 deletions docs/en/docs/tutorial/response-model.md
@@ -1,6 +1,51 @@
# Response Model
# Response Model - Return Type

You can declare the model used for the response with the parameter `response_model` in any of the *path operations*:
You can declare the type used for the response by annotating the *path operation function* **return type**.

You can use **type annotations** the same way you would for input data in function **parameters**, you can use Pydantic models, lists, dictionaries, scalar values like integers, booleans, etc.

=== "Python 3.6 and above"

```Python hl_lines="18 23"
{!> ../../../docs_src/response_model/tutorial001_01.py!}
```

=== "Python 3.9 and above"

```Python hl_lines="18 23"
{!> ../../../docs_src/response_model/tutorial001_01_py39.py!}
```

=== "Python 3.10 and above"

```Python hl_lines="16 21"
{!> ../../../docs_src/response_model/tutorial001_01_py310.py!}
```

FastAPI will use this return type to:

* **Validate** the returned data.
* If the data is invalid (e.g. you are missing a field), it means that *your* app code is broken, not returning what it should, and it will return a server error instead of returning incorrect data. This way you and your clients can be certain that they will receive the data and the data shape expected.
* Add a **JSON Schema** for the response, in the OpenAPI *path operation*.
* This will be used by the **automatic docs**.
* It will also be used by automatic client code generation tools.

But most importantly:

* It will **limit and filter** the output data to what is defined in the return type.
* This is particularly important for **security**, we'll see more of that below.

## `response_model` Parameter

There are some cases where you need or want to return some data that is not exactly what the type declares.

For example, you could want to **return a dictionary** or a database object, but **declare it as a Pydantic model**. This way the Pydantic model would do all the data documentation, validation, etc. for the object that you returned (e.g. a dictionary or database object).

If you added the return type annotation, tools and editors would complain with a (correct) error telling you that your function is returning a type (e.g. a dict) that is different from what you declared (e.g. a Pydantic model).

In those cases, you can use the *path operation decorator* parameter `response_model` instead of the return type.

You can use the `response_model` parameter in any of the *path operations*:

* `@app.get()`
* `@app.post()`
Expand All @@ -10,40 +55,39 @@ You can declare the model used for the response with the parameter `response_mod

=== "Python 3.6 and above"

```Python hl_lines="17"
```Python hl_lines="17 22 24-27"
{!> ../../../docs_src/response_model/tutorial001.py!}
```

=== "Python 3.9 and above"

```Python hl_lines="17"
```Python hl_lines="17 22 24-27"
{!> ../../../docs_src/response_model/tutorial001_py39.py!}
```

=== "Python 3.10 and above"

```Python hl_lines="15"
```Python hl_lines="17 22 24-27"
{!> ../../../docs_src/response_model/tutorial001_py310.py!}
```

!!! note
Notice that `response_model` is a parameter of the "decorator" method (`get`, `post`, etc). Not of your *path operation function*, like all the parameters and body.

It receives the same type you would declare for a Pydantic model attribute, so, it can be a Pydantic model, but it can also be, e.g. a `list` of Pydantic models, like `List[Item]`.
`response_model` receives the same type you would declare for a Pydantic model field, so, it can be a Pydantic model, but it can also be, e.g. a `list` of Pydantic models, like `List[Item]`.

FastAPI will use this `response_model` to do all the data documentation, validation, etc. and also to **convert and filter the output data** to its type declaration.

FastAPI will use this `response_model` to:
!!! tip
If you have strict type checks in your editor, mypy, etc, you can declare the function return type as `Any`.

* Convert the output data to its type declaration.
* Validate the data.
* Add a JSON Schema for the response, in the OpenAPI *path operation*.
* Will be used by the automatic documentation systems.
That way you tell the editor that you are intentionally returning anything. But FastAPI will still do the data documentation, validation, filtering, etc. with the `response_model`.

But most importantly:
### `response_model` Priority

* Will limit the output data to that of the model. We'll see how that's important below.
If you declare both a return type and a `response_model`, the `response_model` will take priority and be used by FastAPI.

!!! note "Technical Details"
The response model is declared in this parameter instead of as a function return type annotation, because the path function may not actually return that response model but rather return a `dict`, database object or some other model, and then use the `response_model` to perform the field limiting and serialization.
This way you can add correct type annotations to your functions even when you are returning a type different than the response model, to be used by the editor and tools like mypy. And still you can have FastAPI do the data validation, documentation, etc. using the `response_model`.

## Return the same input data

Expand Down Expand Up @@ -71,24 +115,24 @@ And we are using this model to declare our input and the same model to declare o

=== "Python 3.6 and above"

```Python hl_lines="17-18"
```Python hl_lines="18"
{!> ../../../docs_src/response_model/tutorial002.py!}
```

=== "Python 3.10 and above"

```Python hl_lines="15-16"
```Python hl_lines="16"
{!> ../../../docs_src/response_model/tutorial002_py310.py!}
```

Now, whenever a browser is creating a user with a password, the API will return the same password in the response.

In this case, it might not be a problem, because the user themself is sending the password.
In this case, it might not be a problem, because it's the same user sending the password.

But if we use the same model for another *path operation*, we could be sending our user's passwords to every client.

!!! danger
Never store the plain password of a user or send it in a response.
Never store the plain password of a user or send it in a response like this, unless you know all the caveats and you know what you are doing.

## Add an output model

Expand All @@ -102,7 +146,7 @@ We can instead create an input model with the plaintext password and an output m

=== "Python 3.10 and above"

```Python hl_lines="7 9 14"
```Python hl_lines="9 11 16"
{!> ../../../docs_src/response_model/tutorial003_py310.py!}
```

Expand All @@ -116,7 +160,7 @@ Here, even though our *path operation function* is returning the same input user

=== "Python 3.10 and above"

```Python hl_lines="22"
```Python hl_lines="24"
{!> ../../../docs_src/response_model/tutorial003_py310.py!}
```

Expand All @@ -130,12 +174,66 @@ Here, even though our *path operation function* is returning the same input user

=== "Python 3.10 and above"

```Python hl_lines="20"
```Python hl_lines="22"
{!> ../../../docs_src/response_model/tutorial003_py310.py!}
```

So, **FastAPI** will take care of filtering out all the data that is not declared in the output model (using Pydantic).

### `response_model` or Return Type

In this case, because the two models are different, if we annotated the function return type as `UserOut`, the editor and tools would complain that we are returning an invalid type, as those are different classes.

That's why in this example we have to declare it in the `response_model` parameter.

...but continue reading below to see how to overcome that.

## Return Type and Data Filtering

Let's continue from the previous example. We wanted to **annotate the function with one type** but return something that includes **more data**.

We want FastAPI to keep **filtering** the data using the response model.

In the previous example, because the classes were different, we had to use the `response_model` parameter. But that also means that we don't get the support from the editor and tools checking the function return type.

But in most of the cases where we need to do something like this, we want the model just to **filter/remove** some of the data as in this example.

And in those cases, we can use classes and inheritance to take advantage of function **type annotations** to get better support in the editor and tools, and still get the FastAPI **data filtering**.

=== "Python 3.6 and above"

```Python hl_lines="9-13 15-16 20"
{!> ../../../docs_src/response_model/tutorial003_01.py!}
```

=== "Python 3.10 and above"

```Python hl_lines="7-10 13-14 18"
{!> ../../../docs_src/response_model/tutorial003_01_py310.py!}
```

With this, we get tooling support, from editors and mypy as this code is correct in terms of types, but we also get the data filtering from FastAPI.

How does this work? Let's check that out. 🤓

### Type Annotations and Tooling

First let's see how editors, mypy and other tools would see this.

`BaseUser` has the base fields. Then `UserIn` inherits from `BaseUser` and adds the `password` field, so, it will include all the fields from both models.

We annotate the function return type as `BaseUser`, but we are actually returning a `UserIn` instance.

The editor, mypy, and other tools won't complain about this because, in typing terms, `UserIn` is a subclass of `BaseUser`, which means it's a *valid* type when what is expected is anything that is a `BaseUser`.

### FastAPI Data Filtering

Now, for FastAPI, it will see the return type and make sure that what you return includes **only** the fields that are declared in the type.

FastAPI does several things internally with Pydantic to make sure that those same rules of class inheritance are not used for the returned data filtering, otherwise you could end up returning much more data than what you expected.

This way, you can get the best of both worlds: type annotations with **tooling support** and **data filtering**.

## See it in the docs

When you see the automatic docs, you can check that the input model and output model will both have their own JSON Schema:
Expand Down
12 changes: 10 additions & 2 deletions docs_src/response_model/tutorial001.py
@@ -1,4 +1,4 @@
from typing import List, Union
from typing import Any, List, Union

from fastapi import FastAPI
from pydantic import BaseModel
Expand All @@ -15,5 +15,13 @@ class Item(BaseModel):


@app.post("/items/", response_model=Item)
async def create_item(item: Item):
async def create_item(item: Item) -> Any:
return item


@app.get("/items/", response_model=List[Item])
async def read_items() -> Any:
return [
{"name": "Portal Gun", "price": 42.0},
{"name": "Plumbus", "price": 32.0},
]
27 changes: 27 additions & 0 deletions docs_src/response_model/tutorial001_01.py
@@ -0,0 +1,27 @@
from typing import List, Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
name: str
description: Union[str, None] = None
price: float
tax: Union[float, None] = None
tags: List[str] = []


@app.post("/items/")
async def create_item(item: Item) -> Item:
return item


@app.get("/items/")
async def read_items() -> List[Item]:
return [
Item(name="Portal Gun", price=42.0),
Item(name="Plumbus", price=32.0),
]
25 changes: 25 additions & 0 deletions docs_src/response_model/tutorial001_01_py310.py
@@ -0,0 +1,25 @@
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
tags: list[str] = []


@app.post("/items/")
async def create_item(item: Item) -> Item:
return item


@app.get("/items/")
async def read_items() -> list[Item]:
return [
Item(name="Portal Gun", price=42.0),
Item(name="Plumbus", price=32.0),
]
27 changes: 27 additions & 0 deletions docs_src/response_model/tutorial001_01_py39.py
@@ -0,0 +1,27 @@
from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
name: str
description: Union[str, None] = None
price: float
tax: Union[float, None] = None
tags: list[str] = []


@app.post("/items/")
async def create_item(item: Item) -> Item:
return item


@app.get("/items/")
async def read_items() -> list[Item]:
return [
Item(name="Portal Gun", price=42.0),
Item(name="Plumbus", price=32.0),
]
12 changes: 11 additions & 1 deletion docs_src/response_model/tutorial001_py310.py
@@ -1,3 +1,5 @@
from typing import Any

from fastapi import FastAPI
from pydantic import BaseModel

Expand All @@ -13,5 +15,13 @@ class Item(BaseModel):


@app.post("/items/", response_model=Item)
async def create_item(item: Item):
async def create_item(item: Item) -> Any:
return item


@app.get("/items/", response_model=list[Item])
async def read_items() -> Any:
return [
{"name": "Portal Gun", "price": 42.0},
{"name": "Plumbus", "price": 32.0},
]
12 changes: 10 additions & 2 deletions docs_src/response_model/tutorial001_py39.py
@@ -1,4 +1,4 @@
from typing import Union
from typing import Any, Union

from fastapi import FastAPI
from pydantic import BaseModel
Expand All @@ -15,5 +15,13 @@ class Item(BaseModel):


@app.post("/items/", response_model=Item)
async def create_item(item: Item):
async def create_item(item: Item) -> Any:
return item


@app.get("/items/", response_model=list[Item])
async def read_items() -> Any:
return [
{"name": "Portal Gun", "price": 42.0},
{"name": "Plumbus", "price": 32.0},
]
4 changes: 2 additions & 2 deletions docs_src/response_model/tutorial002.py
Expand Up @@ -14,6 +14,6 @@ class UserIn(BaseModel):


# Don't do this in production!
@app.post("/user/", response_model=UserIn)
async def create_user(user: UserIn):
@app.post("/user/")
async def create_user(user: UserIn) -> UserIn:
return user
4 changes: 2 additions & 2 deletions docs_src/response_model/tutorial002_py310.py
Expand Up @@ -12,6 +12,6 @@ class UserIn(BaseModel):


# Don't do this in production!
@app.post("/user/", response_model=UserIn)
async def create_user(user: UserIn):
@app.post("/user/")
async def create_user(user: UserIn) -> UserIn:
return user
4 changes: 2 additions & 2 deletions docs_src/response_model/tutorial003.py
@@ -1,4 +1,4 @@
from typing import Union
from typing import Any, Union

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
Expand All @@ -20,5 +20,5 @@ class UserOut(BaseModel):


@app.post("/user/", response_model=UserOut)
async def create_user(user: UserIn):
async def create_user(user: UserIn) -> Any:
return user

0 comments on commit 2938735

Please sign in to comment.