diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml
index b7db15038..c1b1f1fa4 100644
--- a/docs/en/data/external_links.yml
+++ b/docs/en/data/external_links.yml
@@ -1,5 +1,9 @@
articles:
english:
+ - author: Raf Rasenberg
+ author_link: https://rafrasenberg.com/about/
+ link: https://rafrasenberg.com/fastapi-lambda/
+ title: 'FastAPI lambda container: serverless simplified'
- author: Teresa N. Fontanella De Santis
author_link: https://dev.to/
link: https://dev.to/teresafds/authorization-on-fastapi-with-casbin-41og
diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md
index cdafa6899..eab3d56f4 100644
--- a/docs/en/docs/release-notes.md
+++ b/docs/en/docs/release-notes.md
@@ -2,6 +2,22 @@
## Latest Changes
+
+## 0.89.1
+
+### Fixes
+
+* 🐛 Ignore Response classes on return annotation. PR [#5855](https://github.com/tiangolo/fastapi/pull/5855) by [@Kludex](https://github.com/Kludex). See the new docs in the PR below.
+
+### Docs
+
+* 📝 Update docs and examples for Response Model with Return Type Annotations, and update runtime error. PR [#5873](https://github.com/tiangolo/fastapi/pull/5873) by [@tiangolo](https://github.com/tiangolo). New docs at [Response Model - Return Type: Other Return Type Annotations](https://fastapi.tiangolo.com/tutorial/response-model/#other-return-type-annotations).
+* 📝 Add External Link: FastAPI lambda container: serverless simplified. PR [#5784](https://github.com/tiangolo/fastapi/pull/5784) by [@rafrasenberg](https://github.com/rafrasenberg).
+
+### Translations
+
+* 🌐 Add Turkish translation for `docs/tr/docs/tutorial/first_steps.md`. PR [#5691](https://github.com/tiangolo/fastapi/pull/5691) by [@Kadermiyanyedi](https://github.com/Kadermiyanyedi).
+
## 0.89.0
### Features
diff --git a/docs/en/docs/tutorial/response-model.md b/docs/en/docs/tutorial/response-model.md
index 69c02052d..cd7a749d4 100644
--- a/docs/en/docs/tutorial/response-model.md
+++ b/docs/en/docs/tutorial/response-model.md
@@ -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:
@@ -244,6 +246,74 @@ And both models will be used for the interactive API documentation:
+## 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 union 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:
diff --git a/docs/tr/docs/tutorial/first_steps.md b/docs/tr/docs/tutorial/first_steps.md
new file mode 100644
index 000000000..b39802f5d
--- /dev/null
+++ b/docs/tr/docs/tutorial/first_steps.md
@@ -0,0 +1,336 @@
+# İlk Adımlar
+
+En basit FastAPI dosyası şu şekildedir:
+
+```Python
+{!../../../docs_src/first_steps/tutorial001.py!}
+```
+
+Bunu bir `main.py` dosyasına kopyalayın.
+
+Projeyi çalıştırın:
+
+
get
işlemi kullanılarak
+
+
+!!! info "`@decorator` Bilgisi"
+ Python `@something` şeklinde ifadeleri "decorator" olarak adlandırır.
+
+ Decoratoru bir fonksiyonun üzerine koyarsınız. Dekoratif bir şapka gibi (Sanırım terim buradan gelmektedir).
+
+ Bir "decorator" fonksiyonu alır ve bazı işlemler gerçekleştir.
+
+ Bizim durumumzda decarator **FastAPI'ye** fonksiyonun bir `get` işlemi ile `/` pathine geldiğini söyler.
+
+ Bu **path işlem decoratordür**
+
+Ayrıca diğer işlemleri de kullanabilirsiniz:
+
+* `@app.post()`
+* `@app.put()`
+* `@app.delete()`
+
+Ve daha egzotik olanları:
+
+* `@app.options()`
+* `@app.head()`
+* `@app.patch()`
+* `@app.trace()`
+
+!!! tip
+ Her işlemi (HTTP method) istediğiniz gibi kullanmakta özgürsünüz.
+
+ **FastAPI** herhangi bir özel anlamı zorlamaz.
+
+ Buradaki bilgiler bir gereklilik değil, bir kılavuz olarak sunulmaktadır.
+
+ Örneğin, GraphQL kullanırkan normalde tüm işlemleri yalnızca `POST` işlemini kullanarak gerçekleştirirsiniz.
+
+### Adım 4: **path işlem fonksiyonunu** tanımlayın
+
+Aşağıdakiler bizim **path işlem fonksiyonlarımızdır**:
+
+* **path**: `/`
+* **işlem**: `get`
+* **function**: "decorator"ün altındaki fonksiyondur (`@app.get("/")` altında).
+
+```Python hl_lines="7"
+{!../../../docs_src/first_steps/tutorial001.py!}
+```
+
+Bu bir Python fonksiyonudur.
+
+Bir `GET` işlemi kullanarak "`/`" URL'sine bir istek geldiğinde **FastAPI** tarafından çağrılır.
+
+Bu durumda bir `async` fonksiyonudur.
+
+---
+
+Bunu `async def` yerine normal bir fonksiyon olarakta tanımlayabilirsiniz.
+
+```Python hl_lines="7"
+{!../../../docs_src/first_steps/tutorial003.py!}
+```
+
+!!! note
+
+ Eğer farkı bilmiyorsanız, [Async: *"Acelesi var?"*](../async.md#in-a-hurry){.internal-link target=_blank} kontrol edebilirsiniz.
+
+### Adım 5: İçeriği geri döndürün
+
+
+```Python hl_lines="8"
+{!../../../docs_src/first_steps/tutorial001.py!}
+```
+
+Bir `dict`, `list` döndürebilir veya `str`, `int` gibi tekil değerler döndürebilirsiniz.
+
+Ayrıca, Pydantic modellerini de döndürebilirsiniz. (Bununla ilgili daha sonra ayrıntılı bilgi göreceksiniz.)
+
+Otomatik olarak JSON'a dönüştürülecek(ORM'ler vb. dahil) başka birçok nesne ve model vardır. En beğendiklerinizi kullanmayı deneyin, yüksek ihtimalle destekleniyordur.
+
+## Özet
+
+* `FastAPI`'yi içe aktarın.
+* Bir `app` örneği oluşturun.
+* **path işlem decorator** yazın. (`@app.get("/")` gibi)
+* **path işlem fonksiyonu** yazın. (`def root(): ...` gibi)
+* Development sunucunuzu çalıştırın. (`uvicorn main:app --reload` gibi)
diff --git a/docs/tr/mkdocs.yml b/docs/tr/mkdocs.yml
index e29d25936..5904f71f9 100644
--- a/docs/tr/mkdocs.yml
+++ b/docs/tr/mkdocs.yml
@@ -61,6 +61,8 @@ nav:
- features.md
- fastapi-people.md
- python-types.md
+- Tutorial - User Guide:
+ - tutorial/first-steps.md
markdown_extensions:
- toc:
permalink: true
diff --git a/docs_src/response_model/tutorial003_02.py b/docs_src/response_model/tutorial003_02.py
new file mode 100644
index 000000000..df6a09646
--- /dev/null
+++ b/docs_src/response_model/tutorial003_02.py
@@ -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."})
diff --git a/docs_src/response_model/tutorial003_03.py b/docs_src/response_model/tutorial003_03.py
new file mode 100644
index 000000000..0d4bd8de5
--- /dev/null
+++ b/docs_src/response_model/tutorial003_03.py
@@ -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")
diff --git a/docs_src/response_model/tutorial003_04.py b/docs_src/response_model/tutorial003_04.py
new file mode 100644
index 000000000..b13a92692
--- /dev/null
+++ b/docs_src/response_model/tutorial003_04.py
@@ -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."}
diff --git a/docs_src/response_model/tutorial003_04_py310.py b/docs_src/response_model/tutorial003_04_py310.py
new file mode 100644
index 000000000..cee49b83e
--- /dev/null
+++ b/docs_src/response_model/tutorial003_04_py310.py
@@ -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."}
diff --git a/docs_src/response_model/tutorial003_05.py b/docs_src/response_model/tutorial003_05.py
new file mode 100644
index 000000000..0962061a6
--- /dev/null
+++ b/docs_src/response_model/tutorial003_05.py
@@ -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."}
diff --git a/docs_src/response_model/tutorial003_05_py310.py b/docs_src/response_model/tutorial003_05_py310.py
new file mode 100644
index 000000000..f1c0f8e12
--- /dev/null
+++ b/docs_src/response_model/tutorial003_05_py310.py
@@ -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."}
diff --git a/fastapi/__init__.py b/fastapi/__init__.py
index bda10f043..07ed78ffa 100644
--- a/fastapi/__init__.py
+++ b/fastapi/__init__.py
@@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
-__version__ = "0.89.0"
+__version__ = "0.89.1"
from starlette import status as status
diff --git a/fastapi/routing.py b/fastapi/routing.py
index 8c73b954f..f131fa903 100644
--- a/fastapi/routing.py
+++ b/fastapi/routing.py
@@ -42,6 +42,7 @@ from fastapi.utils import (
from pydantic import BaseModel
from pydantic.error_wrappers import ErrorWrapper, ValidationError
from pydantic.fields import ModelField, Undefined
+from pydantic.utils import lenient_issubclass
from starlette import routing
from starlette.concurrency import run_in_threadpool
from starlette.exceptions import HTTPException
@@ -356,7 +357,11 @@ class APIRoute(routing.Route):
self.path = path
self.endpoint = endpoint
if isinstance(response_model, DefaultPlaceholder):
- response_model = get_typed_return_annotation(endpoint)
+ return_annotation = get_typed_return_annotation(endpoint)
+ if lenient_issubclass(return_annotation, Response):
+ response_model = None
+ else:
+ response_model = return_annotation
self.response_model = response_model
self.summary = summary
self.response_description = response_description
diff --git a/fastapi/utils.py b/fastapi/utils.py
index b15f6a2cf..391c47d81 100644
--- a/fastapi/utils.py
+++ b/fastapi/utils.py
@@ -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
diff --git a/pyproject.toml b/pyproject.toml
index b29549f5c..7fb8078f9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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 = [
diff --git a/tests/test_response_model_as_return_annotation.py b/tests/test_response_model_as_return_annotation.py
index f2056fecd..e45364149 100644
--- a/tests/test_response_model_as_return_annotation.py
+++ b/tests/test_response_model_as_return_annotation.py
@@ -2,6 +2,8 @@ from typing import List, Union
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
@@ -237,6 +239,16 @@ def no_response_model_annotation_union_return_model2() -> Union[User, Item]:
return Item(name="Foo", price=42.0)
+@app.get("/no_response_model-annotation_response_class")
+def no_response_model_annotation_response_class() -> Response:
+ return Response(content="Foo")
+
+
+@app.get("/no_response_model-annotation_json_response_class")
+def no_response_model_annotation_json_response_class() -> JSONResponse:
+ return JSONResponse(content={"foo": "bar"})
+
+
openapi_schema = {
"openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"},
@@ -789,6 +801,30 @@ openapi_schema = {
},
}
},
+ "/no_response_model-annotation_response_class": {
+ "get": {
+ "summary": "No Response Model Annotation Response Class",
+ "operationId": "no_response_model_annotation_response_class_no_response_model_annotation_response_class_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ }
+ },
+ }
+ },
+ "/no_response_model-annotation_json_response_class": {
+ "get": {
+ "summary": "No Response Model Annotation Json Response Class",
+ "operationId": "no_response_model_annotation_json_response_class_no_response_model_annotation_json_response_class_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ }
+ },
+ }
+ },
},
"components": {
"schemas": {
@@ -1049,3 +1085,27 @@ def test_no_response_model_annotation_union_return_model2():
response = client.get("/no_response_model-annotation_union-return_model2")
assert response.status_code == 200, response.text
assert response.json() == {"name": "Foo", "price": 42.0}
+
+
+def test_no_response_model_annotation_return_class():
+ response = client.get("/no_response_model-annotation_response_class")
+ assert response.status_code == 200, response.text
+ assert response.text == "Foo"
+
+
+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]
diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_01.py b/tests/test_tutorial/test_response_model/test_tutorial003_01.py
new file mode 100644
index 000000000..39a4734ed
--- /dev/null
+++ b/tests/test_tutorial/test_response_model/test_tutorial003_01.py
@@ -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": "foo@example.com",
+ "full_name": "Grave Dohl",
+ },
+ )
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "username": "foo",
+ "email": "foo@example.com",
+ "full_name": "Grave Dohl",
+ }
diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py
new file mode 100644
index 000000000..3a04db6bc
--- /dev/null
+++ b/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py
@@ -0,0 +1,129 @@
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+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"},
+ },
+ },
+ }
+ },
+}
+
+
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.response_model.tutorial003_01_py310 import app
+
+ client = TestClient(app)
+ return client
+
+
+@needs_py310
+def test_openapi_schema(client: TestClient):
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == openapi_schema
+
+
+@needs_py310
+def test_post_user(client: TestClient):
+ response = client.post(
+ "/user/",
+ json={
+ "username": "foo",
+ "password": "fighter",
+ "email": "foo@example.com",
+ "full_name": "Grave Dohl",
+ },
+ )
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "username": "foo",
+ "email": "foo@example.com",
+ "full_name": "Grave Dohl",
+ }
diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_02.py b/tests/test_tutorial/test_response_model/test_tutorial003_02.py
new file mode 100644
index 000000000..d933f871c
--- /dev/null
+++ b/tests/test_tutorial/test_response_model/test_tutorial003_02.py
@@ -0,0 +1,93 @@
+from fastapi.testclient import TestClient
+
+from docs_src.response_model.tutorial003_02 import app
+
+client = TestClient(app)
+
+openapi_schema = {
+ "openapi": "3.0.2",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/portal": {
+ "get": {
+ "summary": "Get Portal",
+ "operationId": "get_portal_portal_get",
+ "parameters": [
+ {
+ "required": False,
+ "schema": {
+ "title": "Teleport",
+ "type": "boolean",
+ "default": False,
+ },
+ "name": "teleport",
+ "in": "query",
+ }
+ ],
+ "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"},
+ }
+ },
+ },
+ "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_get_portal():
+ response = client.get("/portal")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"message": "Here's your interdimensional portal."}
+
+
+def test_get_redirect():
+ response = client.get("/portal", params={"teleport": True}, follow_redirects=False)
+ assert response.status_code == 307, response.text
+ assert response.headers["location"] == "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_03.py b/tests/test_tutorial/test_response_model/test_tutorial003_03.py
new file mode 100644
index 000000000..398eb4765
--- /dev/null
+++ b/tests/test_tutorial/test_response_model/test_tutorial003_03.py
@@ -0,0 +1,36 @@
+from fastapi.testclient import TestClient
+
+from docs_src.response_model.tutorial003_03 import app
+
+client = TestClient(app)
+
+openapi_schema = {
+ "openapi": "3.0.2",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/teleport": {
+ "get": {
+ "summary": "Get Teleport",
+ "operationId": "get_teleport_teleport_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ }
+ },
+ }
+ }
+ },
+}
+
+
+def test_openapi_schema():
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == openapi_schema
+
+
+def test_get_portal():
+ response = client.get("/teleport", follow_redirects=False)
+ assert response.status_code == 307, response.text
+ assert response.headers["location"] == "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_04.py b/tests/test_tutorial/test_response_model/test_tutorial003_04.py
new file mode 100644
index 000000000..4aa80145a
--- /dev/null
+++ b/tests/test_tutorial/test_response_model/test_tutorial003_04.py
@@ -0,0 +1,9 @@
+import pytest
+from fastapi.exceptions import FastAPIError
+
+
+def test_invalid_response_model():
+ with pytest.raises(FastAPIError):
+ from docs_src.response_model.tutorial003_04 import app
+
+ assert app # pragma: no cover
diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_04_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_04_py310.py
new file mode 100644
index 000000000..b876facc8
--- /dev/null
+++ b/tests/test_tutorial/test_response_model/test_tutorial003_04_py310.py
@@ -0,0 +1,12 @@
+import pytest
+from fastapi.exceptions import FastAPIError
+
+from ...utils import needs_py310
+
+
+@needs_py310
+def test_invalid_response_model():
+ with pytest.raises(FastAPIError):
+ from docs_src.response_model.tutorial003_04_py310 import app
+
+ assert app # pragma: no cover
diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_05.py b/tests/test_tutorial/test_response_model/test_tutorial003_05.py
new file mode 100644
index 000000000..27896d490
--- /dev/null
+++ b/tests/test_tutorial/test_response_model/test_tutorial003_05.py
@@ -0,0 +1,93 @@
+from fastapi.testclient import TestClient
+
+from docs_src.response_model.tutorial003_05 import app
+
+client = TestClient(app)
+
+openapi_schema = {
+ "openapi": "3.0.2",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/portal": {
+ "get": {
+ "summary": "Get Portal",
+ "operationId": "get_portal_portal_get",
+ "parameters": [
+ {
+ "required": False,
+ "schema": {
+ "title": "Teleport",
+ "type": "boolean",
+ "default": False,
+ },
+ "name": "teleport",
+ "in": "query",
+ }
+ ],
+ "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"},
+ }
+ },
+ },
+ "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_get_portal():
+ response = client.get("/portal")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"message": "Here's your interdimensional portal."}
+
+
+def test_get_redirect():
+ response = client.get("/portal", params={"teleport": True}, follow_redirects=False)
+ assert response.status_code == 307, response.text
+ assert response.headers["location"] == "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_05_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_05_py310.py
new file mode 100644
index 000000000..bf36c906b
--- /dev/null
+++ b/tests/test_tutorial/test_response_model/test_tutorial003_05_py310.py
@@ -0,0 +1,103 @@
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+openapi_schema = {
+ "openapi": "3.0.2",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/portal": {
+ "get": {
+ "summary": "Get Portal",
+ "operationId": "get_portal_portal_get",
+ "parameters": [
+ {
+ "required": False,
+ "schema": {
+ "title": "Teleport",
+ "type": "boolean",
+ "default": False,
+ },
+ "name": "teleport",
+ "in": "query",
+ }
+ ],
+ "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"},
+ }
+ },
+ },
+ "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"},
+ },
+ },
+ }
+ },
+}
+
+
+@pytest.fixture(name="client")
+def get_client():
+ from docs_src.response_model.tutorial003_05_py310 import app
+
+ client = TestClient(app)
+ return client
+
+
+@needs_py310
+def test_openapi_schema(client: TestClient):
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == openapi_schema
+
+
+@needs_py310
+def test_get_portal(client: TestClient):
+ response = client.get("/portal")
+ assert response.status_code == 200, response.text
+ assert response.json() == {"message": "Here's your interdimensional portal."}
+
+
+@needs_py310
+def test_get_redirect(client: TestClient):
+ response = client.get("/portal", params={"teleport": True}, follow_redirects=False)
+ assert response.status_code == 307, response.text
+ assert response.headers["location"] == "https://www.youtube.com/watch?v=dQw4w9WgXcQ"