Skip to content

Commit

Permalink
📝 Update docs and examples for Response Model with Return Type Annota…
Browse files Browse the repository at this point in the history
…tions, and update runtime error (#5873)
  • Loading branch information
tiangolo authored Jan 10, 2023
1 parent 6b83525 commit fb8e908
Show file tree
Hide file tree
Showing 18 changed files with 757 additions and 1 deletion.
70 changes: 70 additions & 0 deletions docs/en/docs/tutorial/response-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ If you declare both a return type and a `response_model`, the `response_model` w

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

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.

## Return the same input data

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

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

## Other Return Type Annotations

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).

### Return a Response Directly

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}.

```Python hl_lines="8 10-11"
{!> ../../../docs_src/response_model/tutorial003_02.py!}
```

This simple case is handled automatically by FastAPI because the return type annotation is the class (or a subclass) of `Response`.

And tools will also be happy because both `RedirectResponse` and `JSONResponse` are subclasses of `Response`, so the type annotation is correct.

### Annotate a Response Subclass

You can also use a subclass of `Response` in the type annotation:

```Python hl_lines="8-9"
{!> ../../../docs_src/response_model/tutorial003_03.py!}
```

This will also work because `RedirectResponse` is a subclass of `Response`, and FastAPI will automatically handle this simple case.

### Invalid Return Type Annotations

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.

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 💥:

=== "Python 3.6 and above"

```Python hl_lines="10"
{!> ../../../docs_src/response_model/tutorial003_04.py!}
```

=== "Python 3.10 and above"

```Python hl_lines="8"
{!> ../../../docs_src/response_model/tutorial003_04_py310.py!}
```

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

### Disable Response Model

Continuing from the example above, you might not want to have the default data validation, documentation, filtering, etc. that is performed by FastAPI.

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).

In this case, you can disable the response model generation by setting `response_model=None`:

=== "Python 3.6 and above"

```Python hl_lines="9"
{!> ../../../docs_src/response_model/tutorial003_05.py!}
```

=== "Python 3.10 and above"

```Python hl_lines="7"
{!> ../../../docs_src/response_model/tutorial003_05_py310.py!}
```

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. 🤓

## Response Model encoding parameters

Your response model could have default values, like:
Expand Down
11 changes: 11 additions & 0 deletions docs_src/response_model/tutorial003_02.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from fastapi import FastAPI, Response
from fastapi.responses import JSONResponse, RedirectResponse

app = FastAPI()


@app.get("/portal")
async def get_portal(teleport: bool = False) -> Response:
if teleport:
return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
return JSONResponse(content={"message": "Here's your interdimensional portal."})
9 changes: 9 additions & 0 deletions docs_src/response_model/tutorial003_03.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from fastapi import FastAPI
from fastapi.responses import RedirectResponse

app = FastAPI()


@app.get("/teleport")
async def get_teleport() -> RedirectResponse:
return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
13 changes: 13 additions & 0 deletions docs_src/response_model/tutorial003_04.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from typing import Union

from fastapi import FastAPI, Response
from fastapi.responses import RedirectResponse

app = FastAPI()


@app.get("/portal")
async def get_portal(teleport: bool = False) -> Union[Response, dict]:
if teleport:
return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
return {"message": "Here's your interdimensional portal."}
11 changes: 11 additions & 0 deletions docs_src/response_model/tutorial003_04_py310.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from fastapi import FastAPI, Response
from fastapi.responses import RedirectResponse

app = FastAPI()


@app.get("/portal")
async def get_portal(teleport: bool = False) -> Response | dict:
if teleport:
return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
return {"message": "Here's your interdimensional portal."}
13 changes: 13 additions & 0 deletions docs_src/response_model/tutorial003_05.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from typing import Union

from fastapi import FastAPI, Response
from fastapi.responses import RedirectResponse

app = FastAPI()


@app.get("/portal", response_model=None)
async def get_portal(teleport: bool = False) -> Union[Response, dict]:
if teleport:
return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
return {"message": "Here's your interdimensional portal."}
11 changes: 11 additions & 0 deletions docs_src/response_model/tutorial003_05_py310.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from fastapi import FastAPI, Response
from fastapi.responses import RedirectResponse

app = FastAPI()


@app.get("/portal", response_model=None)
async def get_portal(teleport: bool = False) -> Response | dict:
if teleport:
return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")
return {"message": "Here's your interdimensional portal."}
8 changes: 7 additions & 1 deletion fastapi/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,13 @@ def create_response_field(
return response_field(field_info=field_info)
except RuntimeError:
raise fastapi.exceptions.FastAPIError(
f"Invalid args for response field! Hint: check that {type_} is a valid pydantic field type"
"Invalid args for response field! Hint: "
f"check that {type_} is a valid Pydantic field type. "
"If you are using a return type annotation that is not a valid Pydantic "
"field (e.g. Union[Response, dict, None]) you can disable generating the "
"response model from the type annotation with the path operation decorator "
"parameter response_model=None. Read more: "
"https://fastapi.tiangolo.com/tutorial/response-model/"
) from None


Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ source = [
"fastapi"
]
context = '${CONTEXT}'
omit = [
"docs_src/response_model/tutorial003_04.py",
"docs_src/response_model/tutorial003_04_py310.py",
]

[tool.ruff]
select = [
Expand Down
13 changes: 13 additions & 0 deletions tests/test_response_model_as_return_annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import pytest
from fastapi import FastAPI
from fastapi.exceptions import FastAPIError
from fastapi.responses import JSONResponse, Response
from fastapi.testclient import TestClient
from pydantic import BaseModel, ValidationError
Expand Down Expand Up @@ -1096,3 +1097,15 @@ def test_no_response_model_annotation_json_response_class():
response = client.get("/no_response_model-annotation_json_response_class")
assert response.status_code == 200, response.text
assert response.json() == {"foo": "bar"}


def test_invalid_response_model_field():
app = FastAPI()
with pytest.raises(FastAPIError) as e:

@app.get("/")
def read_root() -> Union[Response, None]:
return Response(content="Foo") # pragma: no cover

assert "valid Pydantic field type" in e.value.args[0]
assert "parameter response_model=None" in e.value.args[0]
120 changes: 120 additions & 0 deletions tests/test_tutorial/test_response_model/test_tutorial003_01.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
from fastapi.testclient import TestClient

from docs_src.response_model.tutorial003_01 import app

client = TestClient(app)

openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
"/user/": {
"post": {
"summary": "Create User",
"operationId": "create_user_user__post",
"requestBody": {
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/UserIn"}
}
},
"required": True,
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/BaseUser"}
}
},
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
},
},
}
}
},
"components": {
"schemas": {
"BaseUser": {
"title": "BaseUser",
"required": ["username", "email"],
"type": "object",
"properties": {
"username": {"title": "Username", "type": "string"},
"email": {"title": "Email", "type": "string", "format": "email"},
"full_name": {"title": "Full Name", "type": "string"},
},
},
"HTTPValidationError": {
"title": "HTTPValidationError",
"type": "object",
"properties": {
"detail": {
"title": "Detail",
"type": "array",
"items": {"$ref": "#/components/schemas/ValidationError"},
}
},
},
"UserIn": {
"title": "UserIn",
"required": ["username", "email", "password"],
"type": "object",
"properties": {
"username": {"title": "Username", "type": "string"},
"email": {"title": "Email", "type": "string", "format": "email"},
"full_name": {"title": "Full Name", "type": "string"},
"password": {"title": "Password", "type": "string"},
},
},
"ValidationError": {
"title": "ValidationError",
"required": ["loc", "msg", "type"],
"type": "object",
"properties": {
"loc": {
"title": "Location",
"type": "array",
"items": {"anyOf": [{"type": "string"}, {"type": "integer"}]},
},
"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, response.text
assert response.json() == openapi_schema


def test_post_user():
response = client.post(
"/user/",
json={
"username": "foo",
"password": "fighter",
"email": "[email protected]",
"full_name": "Grave Dohl",
},
)
assert response.status_code == 200, response.text
assert response.json() == {
"username": "foo",
"email": "[email protected]",
"full_name": "Grave Dohl",
}
Loading

0 comments on commit fb8e908

Please sign in to comment.