Skip to content

Commit ea43f22

Browse files
authored
✨ Add support for disabling the separation of input and output JSON Schemas in OpenAPI with Pydantic v2 (fastapi#10145)
* 📝 Add docs for Separate OpenAPI Schemas for Input and Output * 🔧 Add new docs page to MkDocs config * ✨ Add separate_input_output_schemas parameter to FastAPI class * 📝 Add source examples for separating OpenAPI schemas * ✅ Add tests for separated OpenAPI schemas * 📝 Add source examples for Python 3.10, 3.9, and 3.7+ * 📝 Update docs for Separate OpenAPI Schemas with new multi-version examples * ✅ Add and update tests for different Python versions * ✅ Add tests for corner cases with separate_input_output_schemas * 📝 Update tutorial to use Union instead of Optional * 🐛 Fix type annotations * 🐛 Fix correct import in test * 💄 Add CSS to simulate browser windows for screenshots * ➕ Add playwright as a dev dependency to automate generating screenshots * 🔨 Add Playwright scripts to generate screenshots for new docs * 📝 Update docs, tweak text to match screenshots * 🍱 Add screenshots for new docs
1 parent 10a127e commit ea43f22

31 files changed

Lines changed: 1950 additions & 2 deletions

docs/en/docs/css/custom.css

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,39 @@ code {
144144
margin-top: 2em;
145145
margin-bottom: 2em;
146146
}
147+
148+
/* Screenshots */
149+
/*
150+
Simulate a browser window frame.
151+
Inspired by Termynal's CSS tricks with modifications
152+
*/
153+
154+
.screenshot {
155+
display: block;
156+
background-color: #d3e0de;
157+
border-radius: 4px;
158+
padding: 45px 5px 5px;
159+
position: relative;
160+
-webkit-box-sizing: border-box;
161+
box-sizing: border-box;
162+
}
163+
164+
.screenshot img {
165+
display: block;
166+
border-radius: 2px;
167+
}
168+
169+
.screenshot:before {
170+
content: '';
171+
position: absolute;
172+
top: 15px;
173+
left: 15px;
174+
display: inline-block;
175+
width: 15px;
176+
height: 15px;
177+
border-radius: 50%;
178+
/* A little hack to display the window buttons in one pseudo element. */
179+
background: #d9515d;
180+
-webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;
181+
box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;
182+
}
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
# Separate OpenAPI Schemas for Input and Output or Not
2+
3+
When using **Pydantic v2**, the generated OpenAPI is a bit more exact and **correct** than before. 😎
4+
5+
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**.
6+
7+
Let's see how that works and how to change it if you need to do that.
8+
9+
## Pydantic Models for Input and Output
10+
11+
Let's say you have a Pydantic model with default values, like this one:
12+
13+
=== "Python 3.10+"
14+
15+
```Python hl_lines="7"
16+
{!> ../../../docs_src/separate_openapi_schemas/tutorial001_py310.py[ln:1-7]!}
17+
18+
# Code below omitted 👇
19+
```
20+
21+
<details>
22+
<summary>👀 Full file preview</summary>
23+
24+
```Python
25+
{!> ../../../docs_src/separate_openapi_schemas/tutorial001_py310.py!}
26+
```
27+
28+
</details>
29+
30+
=== "Python 3.9+"
31+
32+
```Python hl_lines="9"
33+
{!> ../../../docs_src/separate_openapi_schemas/tutorial001_py39.py[ln:1-9]!}
34+
35+
# Code below omitted 👇
36+
```
37+
38+
<details>
39+
<summary>👀 Full file preview</summary>
40+
41+
```Python
42+
{!> ../../../docs_src/separate_openapi_schemas/tutorial001_py39.py!}
43+
```
44+
45+
</details>
46+
47+
=== "Python 3.7+"
48+
49+
```Python hl_lines="9"
50+
{!> ../../../docs_src/separate_openapi_schemas/tutorial001.py[ln:1-9]!}
51+
52+
# Code below omitted 👇
53+
```
54+
55+
<details>
56+
<summary>👀 Full file preview</summary>
57+
58+
```Python
59+
{!> ../../../docs_src/separate_openapi_schemas/tutorial001.py!}
60+
```
61+
62+
</details>
63+
64+
### Model for Input
65+
66+
If you use this model as an input like here:
67+
68+
=== "Python 3.10+"
69+
70+
```Python hl_lines="14"
71+
{!> ../../../docs_src/separate_openapi_schemas/tutorial001_py310.py[ln:1-15]!}
72+
73+
# Code below omitted 👇
74+
```
75+
76+
<details>
77+
<summary>👀 Full file preview</summary>
78+
79+
```Python
80+
{!> ../../../docs_src/separate_openapi_schemas/tutorial001_py310.py!}
81+
```
82+
83+
</details>
84+
85+
=== "Python 3.9+"
86+
87+
```Python hl_lines="16"
88+
{!> ../../../docs_src/separate_openapi_schemas/tutorial001_py39.py[ln:1-17]!}
89+
90+
# Code below omitted 👇
91+
```
92+
93+
<details>
94+
<summary>👀 Full file preview</summary>
95+
96+
```Python
97+
{!> ../../../docs_src/separate_openapi_schemas/tutorial001_py39.py!}
98+
```
99+
100+
</details>
101+
102+
=== "Python 3.7+"
103+
104+
```Python hl_lines="16"
105+
{!> ../../../docs_src/separate_openapi_schemas/tutorial001.py[ln:1-17]!}
106+
107+
# Code below omitted 👇
108+
```
109+
110+
<details>
111+
<summary>👀 Full file preview</summary>
112+
113+
```Python
114+
{!> ../../../docs_src/separate_openapi_schemas/tutorial001.py!}
115+
```
116+
117+
</details>
118+
119+
...then the `description` field will **not be required**. Because it has a default value of `None`.
120+
121+
### Input Model in Docs
122+
123+
You can confirm that in the docs, the `description` field doesn't have a **red asterisk**, it's not marked as required:
124+
125+
<div class="screenshot">
126+
<img src="/img/tutorial/separate-openapi-schemas/image01.png">
127+
</div>
128+
129+
### Model for Output
130+
131+
But if you use the same model as an output, like here:
132+
133+
=== "Python 3.10+"
134+
135+
```Python hl_lines="19"
136+
{!> ../../../docs_src/separate_openapi_schemas/tutorial001_py310.py!}
137+
```
138+
139+
=== "Python 3.9+"
140+
141+
```Python hl_lines="21"
142+
{!> ../../../docs_src/separate_openapi_schemas/tutorial001_py39.py!}
143+
```
144+
145+
=== "Python 3.7+"
146+
147+
```Python hl_lines="21"
148+
{!> ../../../docs_src/separate_openapi_schemas/tutorial001.py!}
149+
```
150+
151+
...then because `description` has a default value, if you **don't return anything** for that field, it will still have that **default value**.
152+
153+
### Model for Output Response Data
154+
155+
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`):
156+
157+
<div class="screenshot">
158+
<img src="/img/tutorial/separate-openapi-schemas/image02.png">
159+
</div>
160+
161+
This means that it will **always have a value**, it's just that sometimes the value could be `None` (or `null` in JSON).
162+
163+
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`.
164+
165+
The way to describe this in OpenAPI, is to mark that field as **required**, because it will always be there.
166+
167+
Because of that, the JSON Schema for a model can be different depending on if it's used for **input or output**:
168+
169+
* for **input** the `description` will **not be required**
170+
* for **output** it will be **required** (and possibly `None`, or in JSON terms, `null`)
171+
172+
### Model for Output in Docs
173+
174+
You can check the output model in the docs too, **both** `name` and `description` are marked as **required** with a **red asterisk**:
175+
176+
<div class="screenshot">
177+
<img src="/img/tutorial/separate-openapi-schemas/image03.png">
178+
</div>
179+
180+
### Model for Input and Output in Docs
181+
182+
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`.
183+
184+
For `Item-Input`, `description` is **not required**, it doesn't have a red asterisk.
185+
186+
But for `Item-Output`, `description` is **required**, it has a red asterisk.
187+
188+
<div class="screenshot">
189+
<img src="/img/tutorial/separate-openapi-schemas/image04.png">
190+
</div>
191+
192+
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. 🎉
193+
194+
## Do not Separate Schemas
195+
196+
Now, there are some cases where you might want to have the **same schema for input and output**.
197+
198+
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.
199+
200+
In that case, you can disable this feature in **FastAPI**, with the parameter `separate_input_output_schemas=False`.
201+
202+
=== "Python 3.10+"
203+
204+
```Python hl_lines="10"
205+
{!> ../../../docs_src/separate_openapi_schemas/tutorial002_py310.py!}
206+
```
207+
208+
=== "Python 3.9+"
209+
210+
```Python hl_lines="12"
211+
{!> ../../../docs_src/separate_openapi_schemas/tutorial002_py39.py!}
212+
```
213+
214+
=== "Python 3.7+"
215+
216+
```Python hl_lines="12"
217+
{!> ../../../docs_src/separate_openapi_schemas/tutorial002.py!}
218+
```
219+
220+
### Same Schema for Input and Output Models in Docs
221+
222+
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**:
223+
224+
<div class="screenshot">
225+
<img src="/img/tutorial/separate-openapi-schemas/image05.png">
226+
</div>
227+
228+
This is the same behavior as in Pydantic v1. 🤓
79.6 KB
Loading
90.3 KB
Loading
83.2 KB
Loading
56.9 KB
Loading
44.5 KB
Loading

docs/en/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ nav:
176176
- how-to/custom-request-and-route.md
177177
- how-to/conditional-openapi.md
178178
- how-to/extending-openapi.md
179+
- how-to/separate-openapi-schemas.md
179180
- how-to/custom-docs-ui-assets.md
180181
- how-to/configure-swagger-ui.md
181182
- project-generation.md
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from typing import List, Union
2+
3+
from fastapi import FastAPI
4+
from pydantic import BaseModel
5+
6+
7+
class Item(BaseModel):
8+
name: str
9+
description: Union[str, None] = None
10+
11+
12+
app = FastAPI()
13+
14+
15+
@app.post("/items/")
16+
def create_item(item: Item):
17+
return item
18+
19+
20+
@app.get("/items/")
21+
def read_items() -> List[Item]:
22+
return [
23+
Item(
24+
name="Portal Gun",
25+
description="Device to travel through the multi-rick-verse",
26+
),
27+
Item(name="Plumbus"),
28+
]
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from fastapi import FastAPI
2+
from pydantic import BaseModel
3+
4+
5+
class Item(BaseModel):
6+
name: str
7+
description: str | None = None
8+
9+
10+
app = FastAPI()
11+
12+
13+
@app.post("/items/")
14+
def create_item(item: Item):
15+
return item
16+
17+
18+
@app.get("/items/")
19+
def read_items() -> list[Item]:
20+
return [
21+
Item(
22+
name="Portal Gun",
23+
description="Device to travel through the multi-rick-verse",
24+
),
25+
Item(name="Plumbus"),
26+
]

0 commit comments

Comments
 (0)