diff --git a/docs/en/docs/css/custom.css b/docs/en/docs/css/custom.css
index 066b51725..187040792 100644
--- a/docs/en/docs/css/custom.css
+++ b/docs/en/docs/css/custom.css
@@ -144,3 +144,39 @@ code {
margin-top: 2em;
margin-bottom: 2em;
}
+
+/* Screenshots */
+/*
+Simulate a browser window frame.
+Inspired by Termynal's CSS tricks with modifications
+*/
+
+.screenshot {
+ display: block;
+ background-color: #d3e0de;
+ border-radius: 4px;
+ padding: 45px 5px 5px;
+ position: relative;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+.screenshot img {
+ display: block;
+ border-radius: 2px;
+}
+
+.screenshot:before {
+ content: '';
+ position: absolute;
+ top: 15px;
+ left: 15px;
+ display: inline-block;
+ width: 15px;
+ height: 15px;
+ border-radius: 50%;
+ /* A little hack to display the window buttons in one pseudo element. */
+ background: #d9515d;
+ -webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;
+ box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;
+}
diff --git a/docs/en/docs/how-to/separate-openapi-schemas.md b/docs/en/docs/how-to/separate-openapi-schemas.md
new file mode 100644
index 000000000..39d96ea39
--- /dev/null
+++ b/docs/en/docs/how-to/separate-openapi-schemas.md
@@ -0,0 +1,228 @@
+# Separate OpenAPI Schemas for Input and Output or Not
+
+When using **Pydantic v2**, the generated OpenAPI is a bit more exact and **correct** than before. 😎
+
+In fact, in some cases, it will even have **two JSON Schemas** in OpenAPI for the same Pydantic model, for input and output, depending on if they have **default values**.
+
+Let's see how that works and how to change it if you need to do that.
+
+## Pydantic Models for Input and Output
+
+Let's say you have a Pydantic model with default values, like this one:
+
+=== "Python 3.10+"
+
+ ```Python hl_lines="7"
+ {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py310.py[ln:1-7]!}
+
+ # Code below omitted 👇
+ ```
+
+
+ 👀 Full file preview
+
+ ```Python
+ {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py310.py!}
+ ```
+
+
+
+=== "Python 3.9+"
+
+ ```Python hl_lines="9"
+ {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py39.py[ln:1-9]!}
+
+ # Code below omitted 👇
+ ```
+
+
+ 👀 Full file preview
+
+ ```Python
+ {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py39.py!}
+ ```
+
+
+
+=== "Python 3.7+"
+
+ ```Python hl_lines="9"
+ {!> ../../../docs_src/separate_openapi_schemas/tutorial001.py[ln:1-9]!}
+
+ # Code below omitted 👇
+ ```
+
+
+ 👀 Full file preview
+
+ ```Python
+ {!> ../../../docs_src/separate_openapi_schemas/tutorial001.py!}
+ ```
+
+
+
+### Model for Input
+
+If you use this model as an input like here:
+
+=== "Python 3.10+"
+
+ ```Python hl_lines="14"
+ {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py310.py[ln:1-15]!}
+
+ # Code below omitted 👇
+ ```
+
+
+ 👀 Full file preview
+
+ ```Python
+ {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py310.py!}
+ ```
+
+
+
+=== "Python 3.9+"
+
+ ```Python hl_lines="16"
+ {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py39.py[ln:1-17]!}
+
+ # Code below omitted 👇
+ ```
+
+
+ 👀 Full file preview
+
+ ```Python
+ {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py39.py!}
+ ```
+
+
+
+=== "Python 3.7+"
+
+ ```Python hl_lines="16"
+ {!> ../../../docs_src/separate_openapi_schemas/tutorial001.py[ln:1-17]!}
+
+ # Code below omitted 👇
+ ```
+
+
+ 👀 Full file preview
+
+ ```Python
+ {!> ../../../docs_src/separate_openapi_schemas/tutorial001.py!}
+ ```
+
+
+
+...then the `description` field will **not be required**. Because it has a default value of `None`.
+
+### Input Model in Docs
+
+You can confirm that in the docs, the `description` field doesn't have a **red asterisk**, it's not marked as required:
+
+
+

+
+
+### Model for Output
+
+But if you use the same model as an output, like here:
+
+=== "Python 3.10+"
+
+ ```Python hl_lines="19"
+ {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py310.py!}
+ ```
+
+=== "Python 3.9+"
+
+ ```Python hl_lines="21"
+ {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py39.py!}
+ ```
+
+=== "Python 3.7+"
+
+ ```Python hl_lines="21"
+ {!> ../../../docs_src/separate_openapi_schemas/tutorial001.py!}
+ ```
+
+...then because `description` has a default value, if you **don't return anything** for that field, it will still have that **default value**.
+
+### Model for Output Response Data
+
+If you interact with the docs and check the response, even though the code didn't add anything in one of the `description` fields, the JSON response contains the default value (`null`):
+
+
+

+
+
+This means that it will **always have a value**, it's just that sometimes the value could be `None` (or `null` in JSON).
+
+That means that, clients using your API don't have to check if the value exists or not, they can **asume the field will always be there**, but just that in some cases it will have the default value of `None`.
+
+The way to describe this in OpenAPI, is to mark that field as **required**, because it will always be there.
+
+Because of that, the JSON Schema for a model can be different depending on if it's used for **input or output**:
+
+* for **input** the `description` will **not be required**
+* for **output** it will be **required** (and possibly `None`, or in JSON terms, `null`)
+
+### Model for Output in Docs
+
+You can check the output model in the docs too, **both** `name` and `description` are marked as **required** with a **red asterisk**:
+
+
+

+
+
+### Model for Input and Output in Docs
+
+And if you check all the available Schemas (JSON Schemas) in OpenAPI, you will see that there are two, one `Item-Input` and one `Item-Output`.
+
+For `Item-Input`, `description` is **not required**, it doesn't have a red asterisk.
+
+But for `Item-Output`, `description` is **required**, it has a red asterisk.
+
+
+

+
+
+With this feature from **Pydantic v2**, your API documentation is more **precise**, and if you have autogenerated clients and SDKs, they will be more precise too, with a better **developer experience** and consistency. 🎉
+
+## Do not Separate Schemas
+
+Now, there are some cases where you might want to have the **same schema for input and output**.
+
+Probably the main use case for this is if you already have some autogenerated client code/SDKs and you don't want to update all the autogenerated client code/SDKs yet, you probably will want to do it at some point, but maybe not right now.
+
+In that case, you can disable this feature in **FastAPI**, with the parameter `separate_input_output_schemas=False`.
+
+=== "Python 3.10+"
+
+ ```Python hl_lines="10"
+ {!> ../../../docs_src/separate_openapi_schemas/tutorial002_py310.py!}
+ ```
+
+=== "Python 3.9+"
+
+ ```Python hl_lines="12"
+ {!> ../../../docs_src/separate_openapi_schemas/tutorial002_py39.py!}
+ ```
+
+=== "Python 3.7+"
+
+ ```Python hl_lines="12"
+ {!> ../../../docs_src/separate_openapi_schemas/tutorial002.py!}
+ ```
+
+### Same Schema for Input and Output Models in Docs
+
+And now there will be one single schema for input and output for the model, only `Item`, and it will have `description` as **not required**:
+
+
+

+
+
+This is the same behavior as in Pydantic v1. 🤓
diff --git a/docs/en/docs/img/tutorial/separate-openapi-schemas/image01.png b/docs/en/docs/img/tutorial/separate-openapi-schemas/image01.png
new file mode 100644
index 000000000..aa085f88d
Binary files /dev/null and b/docs/en/docs/img/tutorial/separate-openapi-schemas/image01.png differ
diff --git a/docs/en/docs/img/tutorial/separate-openapi-schemas/image02.png b/docs/en/docs/img/tutorial/separate-openapi-schemas/image02.png
new file mode 100644
index 000000000..672ef1d2b
Binary files /dev/null and b/docs/en/docs/img/tutorial/separate-openapi-schemas/image02.png differ
diff --git a/docs/en/docs/img/tutorial/separate-openapi-schemas/image03.png b/docs/en/docs/img/tutorial/separate-openapi-schemas/image03.png
new file mode 100644
index 000000000..81340fbec
Binary files /dev/null and b/docs/en/docs/img/tutorial/separate-openapi-schemas/image03.png differ
diff --git a/docs/en/docs/img/tutorial/separate-openapi-schemas/image04.png b/docs/en/docs/img/tutorial/separate-openapi-schemas/image04.png
new file mode 100644
index 000000000..fc2302aa7
Binary files /dev/null and b/docs/en/docs/img/tutorial/separate-openapi-schemas/image04.png differ
diff --git a/docs/en/docs/img/tutorial/separate-openapi-schemas/image05.png b/docs/en/docs/img/tutorial/separate-openapi-schemas/image05.png
new file mode 100644
index 000000000..674dd0b2e
Binary files /dev/null and b/docs/en/docs/img/tutorial/separate-openapi-schemas/image05.png differ
diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml
index f75b84ff5..c56e4c942 100644
--- a/docs/en/mkdocs.yml
+++ b/docs/en/mkdocs.yml
@@ -176,6 +176,7 @@ nav:
- how-to/custom-request-and-route.md
- how-to/conditional-openapi.md
- how-to/extending-openapi.md
+ - how-to/separate-openapi-schemas.md
- how-to/custom-docs-ui-assets.md
- how-to/configure-swagger-ui.md
- project-generation.md
diff --git a/docs_src/separate_openapi_schemas/tutorial001.py b/docs_src/separate_openapi_schemas/tutorial001.py
new file mode 100644
index 000000000..415eef8e2
--- /dev/null
+++ b/docs_src/separate_openapi_schemas/tutorial001.py
@@ -0,0 +1,28 @@
+from typing import List, Union
+
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+
+class Item(BaseModel):
+ name: str
+ description: Union[str, None] = None
+
+
+app = FastAPI()
+
+
+@app.post("/items/")
+def create_item(item: Item):
+ return item
+
+
+@app.get("/items/")
+def read_items() -> List[Item]:
+ return [
+ Item(
+ name="Portal Gun",
+ description="Device to travel through the multi-rick-verse",
+ ),
+ Item(name="Plumbus"),
+ ]
diff --git a/docs_src/separate_openapi_schemas/tutorial001_py310.py b/docs_src/separate_openapi_schemas/tutorial001_py310.py
new file mode 100644
index 000000000..289cb54ed
--- /dev/null
+++ b/docs_src/separate_openapi_schemas/tutorial001_py310.py
@@ -0,0 +1,26 @@
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+
+class Item(BaseModel):
+ name: str
+ description: str | None = None
+
+
+app = FastAPI()
+
+
+@app.post("/items/")
+def create_item(item: Item):
+ return item
+
+
+@app.get("/items/")
+def read_items() -> list[Item]:
+ return [
+ Item(
+ name="Portal Gun",
+ description="Device to travel through the multi-rick-verse",
+ ),
+ Item(name="Plumbus"),
+ ]
diff --git a/docs_src/separate_openapi_schemas/tutorial001_py39.py b/docs_src/separate_openapi_schemas/tutorial001_py39.py
new file mode 100644
index 000000000..63cffd1e3
--- /dev/null
+++ b/docs_src/separate_openapi_schemas/tutorial001_py39.py
@@ -0,0 +1,28 @@
+from typing import Optional
+
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+
+class Item(BaseModel):
+ name: str
+ description: Optional[str] = None
+
+
+app = FastAPI()
+
+
+@app.post("/items/")
+def create_item(item: Item):
+ return item
+
+
+@app.get("/items/")
+def read_items() -> list[Item]:
+ return [
+ Item(
+ name="Portal Gun",
+ description="Device to travel through the multi-rick-verse",
+ ),
+ Item(name="Plumbus"),
+ ]
diff --git a/docs_src/separate_openapi_schemas/tutorial002.py b/docs_src/separate_openapi_schemas/tutorial002.py
new file mode 100644
index 000000000..7df93783b
--- /dev/null
+++ b/docs_src/separate_openapi_schemas/tutorial002.py
@@ -0,0 +1,28 @@
+from typing import List, Union
+
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+
+class Item(BaseModel):
+ name: str
+ description: Union[str, None] = None
+
+
+app = FastAPI(separate_input_output_schemas=False)
+
+
+@app.post("/items/")
+def create_item(item: Item):
+ return item
+
+
+@app.get("/items/")
+def read_items() -> List[Item]:
+ return [
+ Item(
+ name="Portal Gun",
+ description="Device to travel through the multi-rick-verse",
+ ),
+ Item(name="Plumbus"),
+ ]
diff --git a/docs_src/separate_openapi_schemas/tutorial002_py310.py b/docs_src/separate_openapi_schemas/tutorial002_py310.py
new file mode 100644
index 000000000..5db210872
--- /dev/null
+++ b/docs_src/separate_openapi_schemas/tutorial002_py310.py
@@ -0,0 +1,26 @@
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+
+class Item(BaseModel):
+ name: str
+ description: str | None = None
+
+
+app = FastAPI(separate_input_output_schemas=False)
+
+
+@app.post("/items/")
+def create_item(item: Item):
+ return item
+
+
+@app.get("/items/")
+def read_items() -> list[Item]:
+ return [
+ Item(
+ name="Portal Gun",
+ description="Device to travel through the multi-rick-verse",
+ ),
+ Item(name="Plumbus"),
+ ]
diff --git a/docs_src/separate_openapi_schemas/tutorial002_py39.py b/docs_src/separate_openapi_schemas/tutorial002_py39.py
new file mode 100644
index 000000000..50d997d92
--- /dev/null
+++ b/docs_src/separate_openapi_schemas/tutorial002_py39.py
@@ -0,0 +1,28 @@
+from typing import Optional
+
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+
+class Item(BaseModel):
+ name: str
+ description: Optional[str] = None
+
+
+app = FastAPI(separate_input_output_schemas=False)
+
+
+@app.post("/items/")
+def create_item(item: Item):
+ return item
+
+
+@app.get("/items/")
+def read_items() -> list[Item]:
+ return [
+ Item(
+ name="Portal Gun",
+ description="Device to travel through the multi-rick-verse",
+ ),
+ Item(name="Plumbus"),
+ ]
diff --git a/fastapi/_compat.py b/fastapi/_compat.py
index 9ffcaf409..eb55b08f2 100644
--- a/fastapi/_compat.py
+++ b/fastapi/_compat.py
@@ -181,9 +181,13 @@ if PYDANTIC_V2:
field_mapping: Dict[
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
],
+ separate_input_output_schemas: bool = True,
) -> Dict[str, Any]:
+ override_mode: Union[Literal["validation"], None] = (
+ None if separate_input_output_schemas else "validation"
+ )
# This expects that GenerateJsonSchema was already used to generate the definitions
- json_schema = field_mapping[(field, field.mode)]
+ json_schema = field_mapping[(field, override_mode or field.mode)]
if "$ref" not in json_schema:
# TODO remove when deprecating Pydantic v1
# Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207
@@ -200,14 +204,19 @@ if PYDANTIC_V2:
fields: List[ModelField],
schema_generator: GenerateJsonSchema,
model_name_map: ModelNameMap,
+ separate_input_output_schemas: bool = True,
) -> Tuple[
Dict[
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
],
Dict[str, Dict[str, Any]],
]:
+ override_mode: Union[Literal["validation"], None] = (
+ None if separate_input_output_schemas else "validation"
+ )
inputs = [
- (field, field.mode, field._type_adapter.core_schema) for field in fields
+ (field, override_mode or field.mode, field._type_adapter.core_schema)
+ for field in fields
]
field_mapping, definitions = schema_generator.generate_definitions(
inputs=inputs
@@ -429,6 +438,7 @@ else:
field_mapping: Dict[
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
],
+ separate_input_output_schemas: bool = True,
) -> Dict[str, Any]:
# This expects that GenerateJsonSchema was already used to generate the definitions
return field_schema( # type: ignore[no-any-return]
@@ -444,6 +454,7 @@ else:
fields: List[ModelField],
schema_generator: GenerateJsonSchema,
model_name_map: ModelNameMap,
+ separate_input_output_schemas: bool = True,
) -> Tuple[
Dict[
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
diff --git a/fastapi/applications.py b/fastapi/applications.py
index e32cfa03d..b681e50b3 100644
--- a/fastapi/applications.py
+++ b/fastapi/applications.py
@@ -92,6 +92,7 @@ class FastAPI(Starlette):
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
generate_unique_id
),
+ separate_input_output_schemas: bool = True,
**extra: Any,
) -> None:
self.debug = debug
@@ -111,6 +112,7 @@ class FastAPI(Starlette):
self.swagger_ui_init_oauth = swagger_ui_init_oauth
self.swagger_ui_parameters = swagger_ui_parameters
self.servers = servers or []
+ self.separate_input_output_schemas = separate_input_output_schemas
self.extra = extra
self.openapi_version = "3.1.0"
self.openapi_schema: Optional[Dict[str, Any]] = None
@@ -227,6 +229,7 @@ class FastAPI(Starlette):
webhooks=self.webhooks.routes,
tags=self.openapi_tags,
servers=self.servers,
+ separate_input_output_schemas=self.separate_input_output_schemas,
)
return self.openapi_schema
diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py
index e295361e6..9498375fe 100644
--- a/fastapi/openapi/utils.py
+++ b/fastapi/openapi/utils.py
@@ -95,6 +95,7 @@ def get_openapi_operation_parameters(
field_mapping: Dict[
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
],
+ separate_input_output_schemas: bool = True,
) -> List[Dict[str, Any]]:
parameters = []
for param in all_route_params:
@@ -107,6 +108,7 @@ def get_openapi_operation_parameters(
schema_generator=schema_generator,
model_name_map=model_name_map,
field_mapping=field_mapping,
+ separate_input_output_schemas=separate_input_output_schemas,
)
parameter = {
"name": param.alias,
@@ -132,6 +134,7 @@ def get_openapi_operation_request_body(
field_mapping: Dict[
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
],
+ separate_input_output_schemas: bool = True,
) -> Optional[Dict[str, Any]]:
if not body_field:
return None
@@ -141,6 +144,7 @@ def get_openapi_operation_request_body(
schema_generator=schema_generator,
model_name_map=model_name_map,
field_mapping=field_mapping,
+ separate_input_output_schemas=separate_input_output_schemas,
)
field_info = cast(Body, body_field.field_info)
request_media_type = field_info.media_type
@@ -211,6 +215,7 @@ def get_openapi_path(
field_mapping: Dict[
Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
],
+ separate_input_output_schemas: bool = True,
) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]:
path = {}
security_schemes: Dict[str, Any] = {}
@@ -242,6 +247,7 @@ def get_openapi_path(
schema_generator=schema_generator,
model_name_map=model_name_map,
field_mapping=field_mapping,
+ separate_input_output_schemas=separate_input_output_schemas,
)
parameters.extend(operation_parameters)
if parameters:
@@ -263,6 +269,7 @@ def get_openapi_path(
schema_generator=schema_generator,
model_name_map=model_name_map,
field_mapping=field_mapping,
+ separate_input_output_schemas=separate_input_output_schemas,
)
if request_body_oai:
operation["requestBody"] = request_body_oai
@@ -280,6 +287,7 @@ def get_openapi_path(
schema_generator=schema_generator,
model_name_map=model_name_map,
field_mapping=field_mapping,
+ separate_input_output_schemas=separate_input_output_schemas,
)
callbacks[callback.name] = {callback.path: cb_path}
operation["callbacks"] = callbacks
@@ -310,6 +318,7 @@ def get_openapi_path(
schema_generator=schema_generator,
model_name_map=model_name_map,
field_mapping=field_mapping,
+ separate_input_output_schemas=separate_input_output_schemas,
)
else:
response_schema = {}
@@ -343,6 +352,7 @@ def get_openapi_path(
schema_generator=schema_generator,
model_name_map=model_name_map,
field_mapping=field_mapping,
+ separate_input_output_schemas=separate_input_output_schemas,
)
media_type = route_response_media_type or "application/json"
additional_schema = (
@@ -433,6 +443,7 @@ def get_openapi(
terms_of_service: Optional[str] = None,
contact: Optional[Dict[str, Union[str, Any]]] = None,
license_info: Optional[Dict[str, Union[str, Any]]] = None,
+ separate_input_output_schemas: bool = True,
) -> Dict[str, Any]:
info: Dict[str, Any] = {"title": title, "version": version}
if summary:
@@ -459,6 +470,7 @@ def get_openapi(
fields=all_fields,
schema_generator=schema_generator,
model_name_map=model_name_map,
+ separate_input_output_schemas=separate_input_output_schemas,
)
for route in routes or []:
if isinstance(route, routing.APIRoute):
@@ -468,6 +480,7 @@ def get_openapi(
schema_generator=schema_generator,
model_name_map=model_name_map,
field_mapping=field_mapping,
+ separate_input_output_schemas=separate_input_output_schemas,
)
if result:
path, security_schemes, path_definitions = result
@@ -487,6 +500,7 @@ def get_openapi(
schema_generator=schema_generator,
model_name_map=model_name_map,
field_mapping=field_mapping,
+ separate_input_output_schemas=separate_input_output_schemas,
)
if result:
path, security_schemes, path_definitions = result
diff --git a/requirements.txt b/requirements.txt
index 7e746016a..ef25ec483 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,3 +3,5 @@
-r requirements-docs.txt
uvicorn[standard] >=0.12.0,<0.23.0
pre-commit >=2.17.0,<4.0.0
+# For generating screenshots
+playwright
diff --git a/scripts/playwright/separate_openapi_schemas/image01.py b/scripts/playwright/separate_openapi_schemas/image01.py
new file mode 100644
index 000000000..0b40f3bbc
--- /dev/null
+++ b/scripts/playwright/separate_openapi_schemas/image01.py
@@ -0,0 +1,29 @@
+import subprocess
+
+from playwright.sync_api import Playwright, sync_playwright
+
+
+def run(playwright: Playwright) -> None:
+ browser = playwright.chromium.launch(headless=False)
+ context = browser.new_context(viewport={"width": 960, "height": 1080})
+ page = context.new_page()
+ page.goto("http://localhost:8000/docs")
+ page.get_by_text("POST/items/Create Item").click()
+ page.get_by_role("tab", name="Schema").first.click()
+ page.screenshot(
+ path="docs/en/docs/img/tutorial/separate-openapi-schemas/image01.png"
+ )
+
+ # ---------------------
+ context.close()
+ browser.close()
+
+
+process = subprocess.Popen(
+ ["uvicorn", "docs_src.separate_openapi_schemas.tutorial001:app"]
+)
+try:
+ with sync_playwright() as playwright:
+ run(playwright)
+finally:
+ process.terminate()
diff --git a/scripts/playwright/separate_openapi_schemas/image02.py b/scripts/playwright/separate_openapi_schemas/image02.py
new file mode 100644
index 000000000..f76af7ee2
--- /dev/null
+++ b/scripts/playwright/separate_openapi_schemas/image02.py
@@ -0,0 +1,30 @@
+import subprocess
+
+from playwright.sync_api import Playwright, sync_playwright
+
+
+def run(playwright: Playwright) -> None:
+ browser = playwright.chromium.launch(headless=False)
+ context = browser.new_context(viewport={"width": 960, "height": 1080})
+ page = context.new_page()
+ page.goto("http://localhost:8000/docs")
+ page.get_by_text("GET/items/Read Items").click()
+ page.get_by_role("button", name="Try it out").click()
+ page.get_by_role("button", name="Execute").click()
+ page.screenshot(
+ path="docs/en/docs/img/tutorial/separate-openapi-schemas/image02.png"
+ )
+
+ # ---------------------
+ context.close()
+ browser.close()
+
+
+process = subprocess.Popen(
+ ["uvicorn", "docs_src.separate_openapi_schemas.tutorial001:app"]
+)
+try:
+ with sync_playwright() as playwright:
+ run(playwright)
+finally:
+ process.terminate()
diff --git a/scripts/playwright/separate_openapi_schemas/image03.py b/scripts/playwright/separate_openapi_schemas/image03.py
new file mode 100644
index 000000000..127f5c428
--- /dev/null
+++ b/scripts/playwright/separate_openapi_schemas/image03.py
@@ -0,0 +1,30 @@
+import subprocess
+
+from playwright.sync_api import Playwright, sync_playwright
+
+
+def run(playwright: Playwright) -> None:
+ browser = playwright.chromium.launch(headless=False)
+ context = browser.new_context(viewport={"width": 960, "height": 1080})
+ page = context.new_page()
+ page.goto("http://localhost:8000/docs")
+ page.get_by_text("GET/items/Read Items").click()
+ page.get_by_role("tab", name="Schema").click()
+ page.get_by_label("Schema").get_by_role("button", name="Expand all").click()
+ page.screenshot(
+ path="docs/en/docs/img/tutorial/separate-openapi-schemas/image03.png"
+ )
+
+ # ---------------------
+ context.close()
+ browser.close()
+
+
+process = subprocess.Popen(
+ ["uvicorn", "docs_src.separate_openapi_schemas.tutorial001:app"]
+)
+try:
+ with sync_playwright() as playwright:
+ run(playwright)
+finally:
+ process.terminate()
diff --git a/scripts/playwright/separate_openapi_schemas/image04.py b/scripts/playwright/separate_openapi_schemas/image04.py
new file mode 100644
index 000000000..208eaf8a0
--- /dev/null
+++ b/scripts/playwright/separate_openapi_schemas/image04.py
@@ -0,0 +1,29 @@
+import subprocess
+
+from playwright.sync_api import Playwright, sync_playwright
+
+
+def run(playwright: Playwright) -> None:
+ browser = playwright.chromium.launch(headless=False)
+ context = browser.new_context(viewport={"width": 960, "height": 1080})
+ page = context.new_page()
+ page.goto("http://localhost:8000/docs")
+ page.get_by_role("button", name="Item-Input").click()
+ page.get_by_role("button", name="Item-Output").click()
+ page.set_viewport_size({"width": 960, "height": 820})
+ page.screenshot(
+ path="docs/en/docs/img/tutorial/separate-openapi-schemas/image04.png"
+ )
+ # ---------------------
+ context.close()
+ browser.close()
+
+
+process = subprocess.Popen(
+ ["uvicorn", "docs_src.separate_openapi_schemas.tutorial001:app"]
+)
+try:
+ with sync_playwright() as playwright:
+ run(playwright)
+finally:
+ process.terminate()
diff --git a/scripts/playwright/separate_openapi_schemas/image05.py b/scripts/playwright/separate_openapi_schemas/image05.py
new file mode 100644
index 000000000..83966b449
--- /dev/null
+++ b/scripts/playwright/separate_openapi_schemas/image05.py
@@ -0,0 +1,29 @@
+import subprocess
+
+from playwright.sync_api import Playwright, sync_playwright
+
+
+def run(playwright: Playwright) -> None:
+ browser = playwright.chromium.launch(headless=False)
+ context = browser.new_context(viewport={"width": 960, "height": 1080})
+ page = context.new_page()
+ page.goto("http://localhost:8000/docs")
+ page.get_by_role("button", name="Item", exact=True).click()
+ page.set_viewport_size({"width": 960, "height": 700})
+ page.screenshot(
+ path="docs/en/docs/img/tutorial/separate-openapi-schemas/image05.png"
+ )
+
+ # ---------------------
+ context.close()
+ browser.close()
+
+
+process = subprocess.Popen(
+ ["uvicorn", "docs_src.separate_openapi_schemas.tutorial002:app"]
+)
+try:
+ with sync_playwright() as playwright:
+ run(playwright)
+finally:
+ process.terminate()
diff --git a/tests/test_openapi_separate_input_output_schemas.py b/tests/test_openapi_separate_input_output_schemas.py
new file mode 100644
index 000000000..70f4b90d7
--- /dev/null
+++ b/tests/test_openapi_separate_input_output_schemas.py
@@ -0,0 +1,490 @@
+from typing import List, Optional
+
+from fastapi import FastAPI
+from fastapi.testclient import TestClient
+from pydantic import BaseModel
+
+from .utils import needs_pydanticv2
+
+
+class SubItem(BaseModel):
+ subname: str
+ sub_description: Optional[str] = None
+ tags: List[str] = []
+
+
+class Item(BaseModel):
+ name: str
+ description: Optional[str] = None
+ sub: Optional[SubItem] = None
+
+
+def get_app_client(separate_input_output_schemas: bool = True) -> TestClient:
+ app = FastAPI(separate_input_output_schemas=separate_input_output_schemas)
+
+ @app.post("/items/")
+ def create_item(item: Item):
+ return item
+
+ @app.post("/items-list/")
+ def create_item_list(item: List[Item]):
+ return item
+
+ @app.get("/items/")
+ def read_items() -> List[Item]:
+ return [
+ Item(
+ name="Portal Gun",
+ description="Device to travel through the multi-rick-verse",
+ sub=SubItem(subname="subname"),
+ ),
+ Item(name="Plumbus"),
+ ]
+
+ client = TestClient(app)
+ return client
+
+
+def test_create_item():
+ client = get_app_client()
+ client_no = get_app_client(separate_input_output_schemas=False)
+ response = client.post("/items/", json={"name": "Plumbus"})
+ response2 = client_no.post("/items/", json={"name": "Plumbus"})
+ assert response.status_code == response2.status_code == 200, response.text
+ assert (
+ response.json()
+ == response2.json()
+ == {"name": "Plumbus", "description": None, "sub": None}
+ )
+
+
+def test_create_item_with_sub():
+ client = get_app_client()
+ client_no = get_app_client(separate_input_output_schemas=False)
+ data = {
+ "name": "Plumbus",
+ "sub": {"subname": "SubPlumbus", "sub_description": "Sub WTF"},
+ }
+ response = client.post("/items/", json=data)
+ response2 = client_no.post("/items/", json=data)
+ assert response.status_code == response2.status_code == 200, response.text
+ assert (
+ response.json()
+ == response2.json()
+ == {
+ "name": "Plumbus",
+ "description": None,
+ "sub": {"subname": "SubPlumbus", "sub_description": "Sub WTF", "tags": []},
+ }
+ )
+
+
+def test_create_item_list():
+ client = get_app_client()
+ client_no = get_app_client(separate_input_output_schemas=False)
+ data = [
+ {"name": "Plumbus"},
+ {
+ "name": "Portal Gun",
+ "description": "Device to travel through the multi-rick-verse",
+ },
+ ]
+ response = client.post("/items-list/", json=data)
+ response2 = client_no.post("/items-list/", json=data)
+ assert response.status_code == response2.status_code == 200, response.text
+ assert (
+ response.json()
+ == response2.json()
+ == [
+ {"name": "Plumbus", "description": None, "sub": None},
+ {
+ "name": "Portal Gun",
+ "description": "Device to travel through the multi-rick-verse",
+ "sub": None,
+ },
+ ]
+ )
+
+
+def test_read_items():
+ client = get_app_client()
+ client_no = get_app_client(separate_input_output_schemas=False)
+ response = client.get("/items/")
+ response2 = client_no.get("/items/")
+ assert response.status_code == response2.status_code == 200, response.text
+ assert (
+ response.json()
+ == response2.json()
+ == [
+ {
+ "name": "Portal Gun",
+ "description": "Device to travel through the multi-rick-verse",
+ "sub": {"subname": "subname", "sub_description": None, "tags": []},
+ },
+ {"name": "Plumbus", "description": None, "sub": None},
+ ]
+ )
+
+
+@needs_pydanticv2
+def test_openapi_schema():
+ client = get_app_client()
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/items/": {
+ "get": {
+ "summary": "Read Items",
+ "operationId": "read_items_items__get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "items": {
+ "$ref": "#/components/schemas/Item-Output"
+ },
+ "type": "array",
+ "title": "Response Read Items Items Get",
+ }
+ }
+ },
+ }
+ },
+ },
+ "post": {
+ "summary": "Create Item",
+ "operationId": "create_item_items__post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/Item-Input"}
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ },
+ },
+ "/items-list/": {
+ "post": {
+ "summary": "Create Item List",
+ "operationId": "create_item_list_items_list__post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "items": {
+ "$ref": "#/components/schemas/Item-Input"
+ },
+ "type": "array",
+ "title": "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": {
+ "properties": {
+ "detail": {
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ "type": "array",
+ "title": "Detail",
+ }
+ },
+ "type": "object",
+ "title": "HTTPValidationError",
+ },
+ "Item-Input": {
+ "properties": {
+ "name": {"type": "string", "title": "Name"},
+ "description": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Description",
+ },
+ "sub": {
+ "anyOf": [
+ {"$ref": "#/components/schemas/SubItem-Input"},
+ {"type": "null"},
+ ]
+ },
+ },
+ "type": "object",
+ "required": ["name"],
+ "title": "Item",
+ },
+ "Item-Output": {
+ "properties": {
+ "name": {"type": "string", "title": "Name"},
+ "description": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Description",
+ },
+ "sub": {
+ "anyOf": [
+ {"$ref": "#/components/schemas/SubItem-Output"},
+ {"type": "null"},
+ ]
+ },
+ },
+ "type": "object",
+ "required": ["name", "description", "sub"],
+ "title": "Item",
+ },
+ "SubItem-Input": {
+ "properties": {
+ "subname": {"type": "string", "title": "Subname"},
+ "sub_description": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Sub Description",
+ },
+ "tags": {
+ "items": {"type": "string"},
+ "type": "array",
+ "title": "Tags",
+ "default": [],
+ },
+ },
+ "type": "object",
+ "required": ["subname"],
+ "title": "SubItem",
+ },
+ "SubItem-Output": {
+ "properties": {
+ "subname": {"type": "string", "title": "Subname"},
+ "sub_description": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Sub Description",
+ },
+ "tags": {
+ "items": {"type": "string"},
+ "type": "array",
+ "title": "Tags",
+ "default": [],
+ },
+ },
+ "type": "object",
+ "required": ["subname", "sub_description", "tags"],
+ "title": "SubItem",
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ "type": "array",
+ "title": "Location",
+ },
+ "msg": {"type": "string", "title": "Message"},
+ "type": {"type": "string", "title": "Error Type"},
+ },
+ "type": "object",
+ "required": ["loc", "msg", "type"],
+ "title": "ValidationError",
+ },
+ }
+ },
+ }
+
+
+@needs_pydanticv2
+def test_openapi_schema_no_separate():
+ client = get_app_client(separate_input_output_schemas=False)
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/items/": {
+ "get": {
+ "summary": "Read Items",
+ "operationId": "read_items_items__get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "items": {"$ref": "#/components/schemas/Item"},
+ "type": "array",
+ "title": "Response Read Items Items Get",
+ }
+ }
+ },
+ }
+ },
+ },
+ "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"
+ }
+ }
+ },
+ },
+ },
+ },
+ },
+ "/items-list/": {
+ "post": {
+ "summary": "Create Item List",
+ "operationId": "create_item_list_items_list__post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "items": {"$ref": "#/components/schemas/Item"},
+ "type": "array",
+ "title": "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": {
+ "properties": {
+ "detail": {
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ "type": "array",
+ "title": "Detail",
+ }
+ },
+ "type": "object",
+ "title": "HTTPValidationError",
+ },
+ "Item": {
+ "properties": {
+ "name": {"type": "string", "title": "Name"},
+ "description": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Description",
+ },
+ "sub": {
+ "anyOf": [
+ {"$ref": "#/components/schemas/SubItem"},
+ {"type": "null"},
+ ]
+ },
+ },
+ "type": "object",
+ "required": ["name"],
+ "title": "Item",
+ },
+ "SubItem": {
+ "properties": {
+ "subname": {"type": "string", "title": "Subname"},
+ "sub_description": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Sub Description",
+ },
+ "tags": {
+ "items": {"type": "string"},
+ "type": "array",
+ "title": "Tags",
+ "default": [],
+ },
+ },
+ "type": "object",
+ "required": ["subname"],
+ "title": "SubItem",
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ "type": "array",
+ "title": "Location",
+ },
+ "msg": {"type": "string", "title": "Message"},
+ "type": {"type": "string", "title": "Error Type"},
+ },
+ "type": "object",
+ "required": ["loc", "msg", "type"],
+ "title": "ValidationError",
+ },
+ }
+ },
+ }
diff --git a/tests/test_tutorial/test_separate_openapi_schemas/__init__.py b/tests/test_tutorial/test_separate_openapi_schemas/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py
new file mode 100644
index 000000000..8079c1134
--- /dev/null
+++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py
@@ -0,0 +1,147 @@
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_pydanticv2
+
+
+@pytest.fixture(name="client")
+def get_client() -> TestClient:
+ from docs_src.separate_openapi_schemas.tutorial001 import app
+
+ client = TestClient(app)
+ return client
+
+
+def test_create_item(client: TestClient) -> None:
+ response = client.post("/items/", json={"name": "Foo"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "Foo", "description": None}
+
+
+def test_read_items(client: TestClient) -> None:
+ response = client.get("/items/")
+ assert response.status_code == 200, response.text
+ assert response.json() == [
+ {
+ "name": "Portal Gun",
+ "description": "Device to travel through the multi-rick-verse",
+ },
+ {"name": "Plumbus", "description": None},
+ ]
+
+
+@needs_pydanticv2
+def test_openapi_schema(client: TestClient) -> None:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/items/": {
+ "get": {
+ "summary": "Read Items",
+ "operationId": "read_items_items__get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "items": {
+ "$ref": "#/components/schemas/Item-Output"
+ },
+ "type": "array",
+ "title": "Response Read Items Items Get",
+ }
+ }
+ },
+ }
+ },
+ },
+ "post": {
+ "summary": "Create Item",
+ "operationId": "create_item_items__post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/Item-Input"}
+ }
+ },
+ "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": {
+ "properties": {
+ "detail": {
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ "type": "array",
+ "title": "Detail",
+ }
+ },
+ "type": "object",
+ "title": "HTTPValidationError",
+ },
+ "Item-Input": {
+ "properties": {
+ "name": {"type": "string", "title": "Name"},
+ "description": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Description",
+ },
+ },
+ "type": "object",
+ "required": ["name"],
+ "title": "Item",
+ },
+ "Item-Output": {
+ "properties": {
+ "name": {"type": "string", "title": "Name"},
+ "description": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Description",
+ },
+ },
+ "type": "object",
+ "required": ["name", "description"],
+ "title": "Item",
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ "type": "array",
+ "title": "Location",
+ },
+ "msg": {"type": "string", "title": "Message"},
+ "type": {"type": "string", "title": "Error Type"},
+ },
+ "type": "object",
+ "required": ["loc", "msg", "type"],
+ "title": "ValidationError",
+ },
+ }
+ },
+ }
diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py310.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py310.py
new file mode 100644
index 000000000..4fa98ccbe
--- /dev/null
+++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py310.py
@@ -0,0 +1,150 @@
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310, needs_pydanticv2
+
+
+@pytest.fixture(name="client")
+def get_client() -> TestClient:
+ from docs_src.separate_openapi_schemas.tutorial001_py310 import app
+
+ client = TestClient(app)
+ return client
+
+
+@needs_py310
+def test_create_item(client: TestClient) -> None:
+ response = client.post("/items/", json={"name": "Foo"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "Foo", "description": None}
+
+
+@needs_py310
+def test_read_items(client: TestClient) -> None:
+ response = client.get("/items/")
+ assert response.status_code == 200, response.text
+ assert response.json() == [
+ {
+ "name": "Portal Gun",
+ "description": "Device to travel through the multi-rick-verse",
+ },
+ {"name": "Plumbus", "description": None},
+ ]
+
+
+@needs_py310
+@needs_pydanticv2
+def test_openapi_schema(client: TestClient) -> None:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/items/": {
+ "get": {
+ "summary": "Read Items",
+ "operationId": "read_items_items__get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "items": {
+ "$ref": "#/components/schemas/Item-Output"
+ },
+ "type": "array",
+ "title": "Response Read Items Items Get",
+ }
+ }
+ },
+ }
+ },
+ },
+ "post": {
+ "summary": "Create Item",
+ "operationId": "create_item_items__post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/Item-Input"}
+ }
+ },
+ "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": {
+ "properties": {
+ "detail": {
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ "type": "array",
+ "title": "Detail",
+ }
+ },
+ "type": "object",
+ "title": "HTTPValidationError",
+ },
+ "Item-Input": {
+ "properties": {
+ "name": {"type": "string", "title": "Name"},
+ "description": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Description",
+ },
+ },
+ "type": "object",
+ "required": ["name"],
+ "title": "Item",
+ },
+ "Item-Output": {
+ "properties": {
+ "name": {"type": "string", "title": "Name"},
+ "description": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Description",
+ },
+ },
+ "type": "object",
+ "required": ["name", "description"],
+ "title": "Item",
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ "type": "array",
+ "title": "Location",
+ },
+ "msg": {"type": "string", "title": "Message"},
+ "type": {"type": "string", "title": "Error Type"},
+ },
+ "type": "object",
+ "required": ["loc", "msg", "type"],
+ "title": "ValidationError",
+ },
+ }
+ },
+ }
diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py39.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py39.py
new file mode 100644
index 000000000..ad36582ed
--- /dev/null
+++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py39.py
@@ -0,0 +1,150 @@
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py39, needs_pydanticv2
+
+
+@pytest.fixture(name="client")
+def get_client() -> TestClient:
+ from docs_src.separate_openapi_schemas.tutorial001_py39 import app
+
+ client = TestClient(app)
+ return client
+
+
+@needs_py39
+def test_create_item(client: TestClient) -> None:
+ response = client.post("/items/", json={"name": "Foo"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "Foo", "description": None}
+
+
+@needs_py39
+def test_read_items(client: TestClient) -> None:
+ response = client.get("/items/")
+ assert response.status_code == 200, response.text
+ assert response.json() == [
+ {
+ "name": "Portal Gun",
+ "description": "Device to travel through the multi-rick-verse",
+ },
+ {"name": "Plumbus", "description": None},
+ ]
+
+
+@needs_py39
+@needs_pydanticv2
+def test_openapi_schema(client: TestClient) -> None:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/items/": {
+ "get": {
+ "summary": "Read Items",
+ "operationId": "read_items_items__get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "items": {
+ "$ref": "#/components/schemas/Item-Output"
+ },
+ "type": "array",
+ "title": "Response Read Items Items Get",
+ }
+ }
+ },
+ }
+ },
+ },
+ "post": {
+ "summary": "Create Item",
+ "operationId": "create_item_items__post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/Item-Input"}
+ }
+ },
+ "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": {
+ "properties": {
+ "detail": {
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ "type": "array",
+ "title": "Detail",
+ }
+ },
+ "type": "object",
+ "title": "HTTPValidationError",
+ },
+ "Item-Input": {
+ "properties": {
+ "name": {"type": "string", "title": "Name"},
+ "description": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Description",
+ },
+ },
+ "type": "object",
+ "required": ["name"],
+ "title": "Item",
+ },
+ "Item-Output": {
+ "properties": {
+ "name": {"type": "string", "title": "Name"},
+ "description": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Description",
+ },
+ },
+ "type": "object",
+ "required": ["name", "description"],
+ "title": "Item",
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ "type": "array",
+ "title": "Location",
+ },
+ "msg": {"type": "string", "title": "Message"},
+ "type": {"type": "string", "title": "Error Type"},
+ },
+ "type": "object",
+ "required": ["loc", "msg", "type"],
+ "title": "ValidationError",
+ },
+ }
+ },
+ }
diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py
new file mode 100644
index 000000000..d2cf7945b
--- /dev/null
+++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py
@@ -0,0 +1,133 @@
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_pydanticv2
+
+
+@pytest.fixture(name="client")
+def get_client() -> TestClient:
+ from docs_src.separate_openapi_schemas.tutorial002 import app
+
+ client = TestClient(app)
+ return client
+
+
+def test_create_item(client: TestClient) -> None:
+ response = client.post("/items/", json={"name": "Foo"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "Foo", "description": None}
+
+
+def test_read_items(client: TestClient) -> None:
+ response = client.get("/items/")
+ assert response.status_code == 200, response.text
+ assert response.json() == [
+ {
+ "name": "Portal Gun",
+ "description": "Device to travel through the multi-rick-verse",
+ },
+ {"name": "Plumbus", "description": None},
+ ]
+
+
+@needs_pydanticv2
+def test_openapi_schema(client: TestClient) -> None:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/items/": {
+ "get": {
+ "summary": "Read Items",
+ "operationId": "read_items_items__get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "items": {"$ref": "#/components/schemas/Item"},
+ "type": "array",
+ "title": "Response Read Items Items Get",
+ }
+ }
+ },
+ }
+ },
+ },
+ "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": {
+ "properties": {
+ "detail": {
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ "type": "array",
+ "title": "Detail",
+ }
+ },
+ "type": "object",
+ "title": "HTTPValidationError",
+ },
+ "Item": {
+ "properties": {
+ "name": {"type": "string", "title": "Name"},
+ "description": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Description",
+ },
+ },
+ "type": "object",
+ "required": ["name"],
+ "title": "Item",
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ "type": "array",
+ "title": "Location",
+ },
+ "msg": {"type": "string", "title": "Message"},
+ "type": {"type": "string", "title": "Error Type"},
+ },
+ "type": "object",
+ "required": ["loc", "msg", "type"],
+ "title": "ValidationError",
+ },
+ }
+ },
+ }
diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py310.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py310.py
new file mode 100644
index 000000000..89c9ce977
--- /dev/null
+++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py310.py
@@ -0,0 +1,136 @@
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310, needs_pydanticv2
+
+
+@pytest.fixture(name="client")
+def get_client() -> TestClient:
+ from docs_src.separate_openapi_schemas.tutorial002_py310 import app
+
+ client = TestClient(app)
+ return client
+
+
+@needs_py310
+def test_create_item(client: TestClient) -> None:
+ response = client.post("/items/", json={"name": "Foo"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "Foo", "description": None}
+
+
+@needs_py310
+def test_read_items(client: TestClient) -> None:
+ response = client.get("/items/")
+ assert response.status_code == 200, response.text
+ assert response.json() == [
+ {
+ "name": "Portal Gun",
+ "description": "Device to travel through the multi-rick-verse",
+ },
+ {"name": "Plumbus", "description": None},
+ ]
+
+
+@needs_py310
+@needs_pydanticv2
+def test_openapi_schema(client: TestClient) -> None:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/items/": {
+ "get": {
+ "summary": "Read Items",
+ "operationId": "read_items_items__get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "items": {"$ref": "#/components/schemas/Item"},
+ "type": "array",
+ "title": "Response Read Items Items Get",
+ }
+ }
+ },
+ }
+ },
+ },
+ "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": {
+ "properties": {
+ "detail": {
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ "type": "array",
+ "title": "Detail",
+ }
+ },
+ "type": "object",
+ "title": "HTTPValidationError",
+ },
+ "Item": {
+ "properties": {
+ "name": {"type": "string", "title": "Name"},
+ "description": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Description",
+ },
+ },
+ "type": "object",
+ "required": ["name"],
+ "title": "Item",
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ "type": "array",
+ "title": "Location",
+ },
+ "msg": {"type": "string", "title": "Message"},
+ "type": {"type": "string", "title": "Error Type"},
+ },
+ "type": "object",
+ "required": ["loc", "msg", "type"],
+ "title": "ValidationError",
+ },
+ }
+ },
+ }
diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py39.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py39.py
new file mode 100644
index 000000000..6ac3d8f79
--- /dev/null
+++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py39.py
@@ -0,0 +1,136 @@
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py39, needs_pydanticv2
+
+
+@pytest.fixture(name="client")
+def get_client() -> TestClient:
+ from docs_src.separate_openapi_schemas.tutorial002_py39 import app
+
+ client = TestClient(app)
+ return client
+
+
+@needs_py39
+def test_create_item(client: TestClient) -> None:
+ response = client.post("/items/", json={"name": "Foo"})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"name": "Foo", "description": None}
+
+
+@needs_py39
+def test_read_items(client: TestClient) -> None:
+ response = client.get("/items/")
+ assert response.status_code == 200, response.text
+ assert response.json() == [
+ {
+ "name": "Portal Gun",
+ "description": "Device to travel through the multi-rick-verse",
+ },
+ {"name": "Plumbus", "description": None},
+ ]
+
+
+@needs_py39
+@needs_pydanticv2
+def test_openapi_schema(client: TestClient) -> None:
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/items/": {
+ "get": {
+ "summary": "Read Items",
+ "operationId": "read_items_items__get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "items": {"$ref": "#/components/schemas/Item"},
+ "type": "array",
+ "title": "Response Read Items Items Get",
+ }
+ }
+ },
+ }
+ },
+ },
+ "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": {
+ "properties": {
+ "detail": {
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ "type": "array",
+ "title": "Detail",
+ }
+ },
+ "type": "object",
+ "title": "HTTPValidationError",
+ },
+ "Item": {
+ "properties": {
+ "name": {"type": "string", "title": "Name"},
+ "description": {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Description",
+ },
+ },
+ "type": "object",
+ "required": ["name"],
+ "title": "Item",
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ "type": "array",
+ "title": "Location",
+ },
+ "msg": {"type": "string", "title": "Message"},
+ "type": {"type": "string", "title": "Error Type"},
+ },
+ "type": "object",
+ "required": ["loc", "msg", "type"],
+ "title": "ValidationError",
+ },
+ }
+ },
+ }