Skip to content

Commit fb8e908

Browse files
authored
📝 Update docs and examples for Response Model with Return Type Annotations, and update runtime error (#5873)
1 parent 6b83525 commit fb8e908

18 files changed

+757
-1
lines changed

docs/en/docs/tutorial/response-model.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ If you declare both a return type and a `response_model`, the `response_model` w
8989

9090
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`.
9191

92+
You can also use `response_model=None` to disable creating a response model for that *path operation*, you might need to do it if you are adding type annotations for things that are not valid Pydantic fields, you will see an example of that in one of the sections below.
93+
9294
## Return the same input data
9395

9496
Here we are declaring a `UserIn` model, it will contain a plaintext password:
@@ -244,6 +246,74 @@ And both models will be used for the interactive API documentation:
244246

245247
<img src="/img/tutorial/response-model/image02.png">
246248

249+
## Other Return Type Annotations
250+
251+
There might be cases where you return something that is not a valid Pydantic field and you annotate it in the function, only to get the support provided by tooling (the editor, mypy, etc).
252+
253+
### Return a Response Directly
254+
255+
The most common case would be [returning a Response directly as explained later in the advanced docs](../advanced/response-directly.md){.internal-link target=_blank}.
256+
257+
```Python hl_lines="8 10-11"
258+
{!> ../../../docs_src/response_model/tutorial003_02.py!}
259+
```
260+
261+
This simple case is handled automatically by FastAPI because the return type annotation is the class (or a subclass) of `Response`.
262+
263+
And tools will also be happy because both `RedirectResponse` and `JSONResponse` are subclasses of `Response`, so the type annotation is correct.
264+
265+
### Annotate a Response Subclass
266+
267+
You can also use a subclass of `Response` in the type annotation:
268+
269+
```Python hl_lines="8-9"
270+
{!> ../../../docs_src/response_model/tutorial003_03.py!}
271+
```
272+
273+
This will also work because `RedirectResponse` is a subclass of `Response`, and FastAPI will automatically handle this simple case.
274+
275+
### Invalid Return Type Annotations
276+
277+
But when you return some other arbitrary object that is not a valid Pydantic type (e.g. a database object) and you annotate it like that in the function, FastAPI will try to create a Pydantic response model from that type annotation, and will fail.
278+
279+
The same would happen if you had something like a <abbr title='A union between multiple types means "any of these types".'>union</abbr> between different types where one or more of them are not valid Pydantic types, for example this would fail 💥:
280+
281+
=== "Python 3.6 and above"
282+
283+
```Python hl_lines="10"
284+
{!> ../../../docs_src/response_model/tutorial003_04.py!}
285+
```
286+
287+
=== "Python 3.10 and above"
288+
289+
```Python hl_lines="8"
290+
{!> ../../../docs_src/response_model/tutorial003_04_py310.py!}
291+
```
292+
293+
...this fails because the type annotation is not a Pydantic type and is not just a single `Response` class or subclass, it's a union (any of the two) between a `Response` and a `dict`.
294+
295+
### Disable Response Model
296+
297+
Continuing from the example above, you might not want to have the default data validation, documentation, filtering, etc. that is performed by FastAPI.
298+
299+
But you might want to still keep the return type annotation in the function to get the support from tools like editors and type checkers (e.g. mypy).
300+
301+
In this case, you can disable the response model generation by setting `response_model=None`:
302+
303+
=== "Python 3.6 and above"
304+
305+
```Python hl_lines="9"
306+
{!> ../../../docs_src/response_model/tutorial003_05.py!}
307+
```
308+
309+
=== "Python 3.10 and above"
310+
311+
```Python hl_lines="7"
312+
{!> ../../../docs_src/response_model/tutorial003_05_py310.py!}
313+
```
314+
315+
This will make FastAPI skip the response model generation and that way you can have any return type annotations you need without it affecting your FastAPI application. 🤓
316+
247317
## Response Model encoding parameters
248318

249319
Your response model could have default values, like:
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from fastapi import FastAPI, Response
2+
from fastapi.responses import JSONResponse, RedirectResponse
3+
4+
app = FastAPI()
5+
6+
7+
@app.get("/portal")
8+
async def get_portal(teleport: bool = False) -> Response:
9+
if teleport:
10+
return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
11+
return JSONResponse(content={"message": "Here's your interdimensional portal."})
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from fastapi import FastAPI
2+
from fastapi.responses import RedirectResponse
3+
4+
app = FastAPI()
5+
6+
7+
@app.get("/teleport")
8+
async def get_teleport() -> RedirectResponse:
9+
return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from typing import Union
2+
3+
from fastapi import FastAPI, Response
4+
from fastapi.responses import RedirectResponse
5+
6+
app = FastAPI()
7+
8+
9+
@app.get("/portal")
10+
async def get_portal(teleport: bool = False) -> Union[Response, dict]:
11+
if teleport:
12+
return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
13+
return {"message": "Here's your interdimensional portal."}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from fastapi import FastAPI, Response
2+
from fastapi.responses import RedirectResponse
3+
4+
app = FastAPI()
5+
6+
7+
@app.get("/portal")
8+
async def get_portal(teleport: bool = False) -> Response | dict:
9+
if teleport:
10+
return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
11+
return {"message": "Here's your interdimensional portal."}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from typing import Union
2+
3+
from fastapi import FastAPI, Response
4+
from fastapi.responses import RedirectResponse
5+
6+
app = FastAPI()
7+
8+
9+
@app.get("/portal", response_model=None)
10+
async def get_portal(teleport: bool = False) -> Union[Response, dict]:
11+
if teleport:
12+
return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
13+
return {"message": "Here's your interdimensional portal."}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from fastapi import FastAPI, Response
2+
from fastapi.responses import RedirectResponse
3+
4+
app = FastAPI()
5+
6+
7+
@app.get("/portal", response_model=None)
8+
async def get_portal(teleport: bool = False) -> Response | dict:
9+
if teleport:
10+
return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
11+
return {"message": "Here's your interdimensional portal."}

fastapi/utils.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,13 @@ def create_response_field(
8888
return response_field(field_info=field_info)
8989
except RuntimeError:
9090
raise fastapi.exceptions.FastAPIError(
91-
f"Invalid args for response field! Hint: check that {type_} is a valid pydantic field type"
91+
"Invalid args for response field! Hint: "
92+
f"check that {type_} is a valid Pydantic field type. "
93+
"If you are using a return type annotation that is not a valid Pydantic "
94+
"field (e.g. Union[Response, dict, None]) you can disable generating the "
95+
"response model from the type annotation with the path operation decorator "
96+
"parameter response_model=None. Read more: "
97+
"https://fastapi.tiangolo.com/tutorial/response-model/"
9298
) from None
9399

94100

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ source = [
155155
"fastapi"
156156
]
157157
context = '${CONTEXT}'
158+
omit = [
159+
"docs_src/response_model/tutorial003_04.py",
160+
"docs_src/response_model/tutorial003_04_py310.py",
161+
]
158162

159163
[tool.ruff]
160164
select = [

tests/test_response_model_as_return_annotation.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import pytest
44
from fastapi import FastAPI
5+
from fastapi.exceptions import FastAPIError
56
from fastapi.responses import JSONResponse, Response
67
from fastapi.testclient import TestClient
78
from pydantic import BaseModel, ValidationError
@@ -1096,3 +1097,15 @@ def test_no_response_model_annotation_json_response_class():
10961097
response = client.get("/no_response_model-annotation_json_response_class")
10971098
assert response.status_code == 200, response.text
10981099
assert response.json() == {"foo": "bar"}
1100+
1101+
1102+
def test_invalid_response_model_field():
1103+
app = FastAPI()
1104+
with pytest.raises(FastAPIError) as e:
1105+
1106+
@app.get("/")
1107+
def read_root() -> Union[Response, None]:
1108+
return Response(content="Foo") # pragma: no cover
1109+
1110+
assert "valid Pydantic field type" in e.value.args[0]
1111+
assert "parameter response_model=None" in e.value.args[0]

0 commit comments

Comments
 (0)