Skip to content

Commit 9262fa8

Browse files
authored
✨ Add support for not needing ... as default value in required Query(), Path(), Header(), etc. (fastapi#4906)
* ✨ Do not require default value in Query(), Path(), Header(), etc * 📝 Update source examples for docs with default and required values * ✅ Update tests with new default values and not required Ellipsis * 📝 Update docs for Query params and update info about default value, required, Ellipsis
1 parent 31690dd commit 9262fa8

107 files changed

Lines changed: 404 additions & 314 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/en/docs/tutorial/query-params-str-validations.md

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ Let's take this application as example:
1616
{!> ../../../docs_src/query_params_str_validations/tutorial001_py310.py!}
1717
```
1818

19-
The query parameter `q` is of type `Optional[str]` (or `str | None` in Python 3.10), that means that it's of type `str` but could also be `None`, and indeed, the default value is `None`, so FastAPI will know it's not required.
19+
The query parameter `q` is of type `Union[str, None]` (or `str | None` in Python 3.10), that means that it's of type `str` but could also be `None`, and indeed, the default value is `None`, so FastAPI will know it's not required.
2020

2121
!!! note
2222
FastAPI will know that the value of `q` is not required because of the default value `= None`.
2323

24-
The `Optional` in `Optional[str]` is not used by FastAPI, but will allow your editor to give you better support and detect errors.
24+
The `Union` in `Union[str, None]` will allow your editor to give you better support and detect errors.
2525

2626
## Additional validation
2727

@@ -59,24 +59,24 @@ And now use it as the default value of your parameter, setting the parameter `ma
5959
{!> ../../../docs_src/query_params_str_validations/tutorial002_py310.py!}
6060
```
6161

62-
As we have to replace the default value `None` with `Query(None)`, the first parameter to `Query` serves the same purpose of defining that default value.
62+
As we have to replace the default value `None` in the function with `Query()`, we can now set the default value with the parameter `Query(default=None)`, it serves the same purpose of defining that default value.
6363

6464
So:
6565

6666
```Python
67-
q: Optional[str] = Query(None)
67+
q: Union[str, None] = Query(default=None)
6868
```
6969

7070
...makes the parameter optional, the same as:
7171

7272
```Python
73-
q: Optional[str] = None
73+
q: Union[str, None] = None
7474
```
7575

7676
And in Python 3.10 and above:
7777

7878
```Python
79-
q: str | None = Query(None)
79+
q: str | None = Query(default=None)
8080
```
8181

8282
...makes the parameter optional, the same as:
@@ -97,17 +97,17 @@ But it declares it explicitly as being a query parameter.
9797
or the:
9898

9999
```Python
100-
= Query(None)
100+
= Query(default=None)
101101
```
102102

103103
as it will use that `None` as the default value, and that way make the parameter **not required**.
104104

105-
The `Optional` part allows your editor to provide better support, but it is not what tells FastAPI that this parameter is not required.
105+
The `Union[str, None]` part allows your editor to provide better support, but it is not what tells FastAPI that this parameter is not required.
106106

107107
Then, we can pass more parameters to `Query`. In this case, the `max_length` parameter that applies to strings:
108108

109109
```Python
110-
q: str = Query(None, max_length=50)
110+
q: Union[str, None] = Query(default=None, max_length=50)
111111
```
112112

113113
This will validate the data, show a clear error when the data is not valid, and document the parameter in the OpenAPI schema *path operation*.
@@ -118,7 +118,7 @@ You can also add a parameter `min_length`:
118118

119119
=== "Python 3.6 and above"
120120

121-
```Python hl_lines="9"
121+
```Python hl_lines="10"
122122
{!> ../../../docs_src/query_params_str_validations/tutorial003.py!}
123123
```
124124

@@ -134,13 +134,13 @@ You can define a <abbr title="A regular expression, regex or regexp is a sequenc
134134

135135
=== "Python 3.6 and above"
136136

137-
```Python hl_lines="10"
137+
```Python hl_lines="11"
138138
{!> ../../../docs_src/query_params_str_validations/tutorial004.py!}
139139
```
140140

141141
=== "Python 3.10 and above"
142142

143-
```Python hl_lines="8"
143+
```Python hl_lines="9"
144144
{!> ../../../docs_src/query_params_str_validations/tutorial004_py310.py!}
145145
```
146146

@@ -156,7 +156,7 @@ But whenever you need them and go and learn them, know that you can already use
156156

157157
## Default values
158158

159-
The same way that you can pass `None` as the first argument to be used as the default value, you can pass other values.
159+
The same way that you can pass `None` as the value for the `default` parameter, you can pass other values.
160160

161161
Let's say that you want to declare the `q` query parameter to have a `min_length` of `3`, and to have a default value of `"fixedquery"`:
162162

@@ -178,26 +178,68 @@ q: str
178178
instead of:
179179

180180
```Python
181-
q: Optional[str] = None
181+
q: Union[str, None] = None
182182
```
183183

184184
But we are now declaring it with `Query`, for example like:
185185

186186
```Python
187-
q: Optional[str] = Query(None, min_length=3)
187+
q: Union[str, None] = Query(default=None, min_length=3)
188188
```
189189

190-
So, when you need to declare a value as required while using `Query`, you can use `...` as the first argument:
190+
So, when you need to declare a value as required while using `Query`, you can simply not declare a default value:
191191

192192
```Python hl_lines="7"
193193
{!../../../docs_src/query_params_str_validations/tutorial006.py!}
194194
```
195195

196+
### Required with Ellipsis (`...`)
197+
198+
There's an alternative way to explicitly declare that a value is required. You can set the `default` parameter to the literal value `...`:
199+
200+
```Python hl_lines="7"
201+
{!../../../docs_src/query_params_str_validations/tutorial006b.py!}
202+
```
203+
196204
!!! info
197205
If you hadn't seen that `...` before: it is a special single value, it is <a href="https://docs.python.org/3/library/constants.html#Ellipsis" class="external-link" target="_blank">part of Python and is called "Ellipsis"</a>.
198206

207+
It is used by Pydantic and FastAPI to explicitly declare that a value is required.
208+
199209
This will let **FastAPI** know that this parameter is required.
200210

211+
### Required with `None`
212+
213+
You can declare that a parameter can accept `None`, but that it's still required. This would force clients to send a value, even if the value is `None`.
214+
215+
To do that, you can declare that `None` is a valid type but still use `default=...`:
216+
217+
=== "Python 3.6 and above"
218+
219+
```Python hl_lines="8"
220+
{!> ../../../docs_src/query_params_str_validations/tutorial006c.py!}
221+
```
222+
223+
=== "Python 3.10 and above"
224+
225+
```Python hl_lines="7"
226+
{!> ../../../docs_src/query_params_str_validations/tutorial006c_py310.py!}
227+
```
228+
229+
!!! tip
230+
Pydantic, which is what powers all the data validation and serialization in FastAPI, has a special behavior when you use `Optional` or `Union[Something, None]` without a default value, you can read more about it in the Pydantic docs about <a href="https://pydantic-docs.helpmanual.io/usage/models/#required-optional-fields" class="external-link" target="_blank">Required Optional fields</a>.
231+
232+
### Use Pydantic's `Required` instead of Ellipsis (`...`)
233+
234+
If you feel uncomfortable using `...`, you can also import and use `Required` from Pydantic:
235+
236+
```Python hl_lines="2 8"
237+
{!../../../docs_src/query_params_str_validations/tutorial006d.py!}
238+
```
239+
240+
!!! tip
241+
Remember that in most of the cases, when something is required, you can simply omit the `default` parameter, so you normally don't have to use `...` nor `Required`.
242+
201243
## Query parameter list / multiple values
202244

203245
When you define a query parameter explicitly with `Query` you can also declare it to receive a list of values, or said in other way, to receive multiple values.
@@ -315,7 +357,7 @@ You can add a `title`:
315357

316358
=== "Python 3.10 and above"
317359

318-
```Python hl_lines="7"
360+
```Python hl_lines="8"
319361
{!> ../../../docs_src/query_params_str_validations/tutorial007_py310.py!}
320362
```
321363

@@ -399,7 +441,7 @@ To exclude a query parameter from the generated OpenAPI schema (and thus, from t
399441

400442
=== "Python 3.10 and above"
401443

402-
```Python hl_lines="7"
444+
```Python hl_lines="8"
403445
{!> ../../../docs_src/query_params_str_validations/tutorial014_py310.py!}
404446
```
405447

docs_src/additional_status_codes/tutorial001.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Optional
1+
from typing import Union
22

33
from fastapi import Body, FastAPI, status
44
from fastapi.responses import JSONResponse
@@ -10,7 +10,9 @@
1010

1111
@app.put("/items/{item_id}")
1212
async def upsert_item(
13-
item_id: str, name: Optional[str] = Body(None), size: Optional[int] = Body(None)
13+
item_id: str,
14+
name: Union[str, None] = Body(default=None),
15+
size: Union[int, None] = Body(default=None),
1416
):
1517
if item_id in items:
1618
item = items[item_id]

docs_src/app_testing/app_b/main.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Optional
1+
from typing import Union
22

33
from fastapi import FastAPI, Header, HTTPException
44
from pydantic import BaseModel
@@ -16,11 +16,11 @@
1616
class Item(BaseModel):
1717
id: str
1818
title: str
19-
description: Optional[str] = None
19+
description: Union[str, None] = None
2020

2121

2222
@app.get("/items/{item_id}", response_model=Item)
23-
async def read_main(item_id: str, x_token: str = Header(...)):
23+
async def read_main(item_id: str, x_token: str = Header()):
2424
if x_token != fake_secret_token:
2525
raise HTTPException(status_code=400, detail="Invalid X-Token header")
2626
if item_id not in fake_db:
@@ -29,7 +29,7 @@ async def read_main(item_id: str, x_token: str = Header(...)):
2929

3030

3131
@app.post("/items/", response_model=Item)
32-
async def create_item(item: Item, x_token: str = Header(...)):
32+
async def create_item(item: Item, x_token: str = Header()):
3333
if x_token != fake_secret_token:
3434
raise HTTPException(status_code=400, detail="Invalid X-Token header")
3535
if item.id in fake_db:

docs_src/app_testing/app_b_py310/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class Item(BaseModel):
1818

1919

2020
@app.get("/items/{item_id}", response_model=Item)
21-
async def read_main(item_id: str, x_token: str = Header(...)):
21+
async def read_main(item_id: str, x_token: str = Header()):
2222
if x_token != fake_secret_token:
2323
raise HTTPException(status_code=400, detail="Invalid X-Token header")
2424
if item_id not in fake_db:
@@ -27,7 +27,7 @@ async def read_main(item_id: str, x_token: str = Header(...)):
2727

2828

2929
@app.post("/items/", response_model=Item)
30-
async def create_item(item: Item, x_token: str = Header(...)):
30+
async def create_item(item: Item, x_token: str = Header()):
3131
if x_token != fake_secret_token:
3232
raise HTTPException(status_code=400, detail="Invalid X-Token header")
3333
if item.id in fake_db:

docs_src/bigger_applications/app/dependencies.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from fastapi import Header, HTTPException
22

33

4-
async def get_token_header(x_token: str = Header(...)):
4+
async def get_token_header(x_token: str = Header()):
55
if x_token != "fake-super-secret-token":
66
raise HTTPException(status_code=400, detail="X-Token header invalid")
77

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Optional
1+
from typing import Union
22

33
from fastapi import Body, FastAPI
44
from pydantic import BaseModel, Field
@@ -8,14 +8,14 @@
88

99
class Item(BaseModel):
1010
name: str
11-
description: Optional[str] = Field(
12-
None, title="The description of the item", max_length=300
11+
description: Union[str, None] = Field(
12+
default=None, title="The description of the item", max_length=300
1313
)
14-
price: float = Field(..., gt=0, description="The price must be greater than zero")
15-
tax: Optional[float] = None
14+
price: float = Field(gt=0, description="The price must be greater than zero")
15+
tax: Union[float, None] = None
1616

1717

1818
@app.put("/items/{item_id}")
19-
async def update_item(item_id: int, item: Item = Body(..., embed=True)):
19+
async def update_item(item_id: int, item: Item = Body(embed=True)):
2020
results = {"item_id": item_id, "item": item}
2121
return results

docs_src/body_fields/tutorial001_py310.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
class Item(BaseModel):
88
name: str
99
description: str | None = Field(
10-
None, title="The description of the item", max_length=300
10+
default=None, title="The description of the item", max_length=300
1111
)
12-
price: float = Field(..., gt=0, description="The price must be greater than zero")
12+
price: float = Field(gt=0, description="The price must be greater than zero")
1313
tax: float | None = None
1414

1515

1616
@app.put("/items/{item_id}")
17-
async def update_item(item_id: int, item: Item = Body(..., embed=True)):
17+
async def update_item(item_id: int, item: Item = Body(embed=True)):
1818
results = {"item_id": item_id, "item": item}
1919
return results

docs_src/body_multiple_params/tutorial001.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Optional
1+
from typing import Union
22

33
from fastapi import FastAPI, Path
44
from pydantic import BaseModel
@@ -8,17 +8,17 @@
88

99
class Item(BaseModel):
1010
name: str
11-
description: Optional[str] = None
11+
description: Union[str, None] = None
1212
price: float
13-
tax: Optional[float] = None
13+
tax: Union[float, None] = None
1414

1515

1616
@app.put("/items/{item_id}")
1717
async def update_item(
1818
*,
19-
item_id: int = Path(..., title="The ID of the item to get", ge=0, le=1000),
20-
q: Optional[str] = None,
21-
item: Optional[Item] = None,
19+
item_id: int = Path(title="The ID of the item to get", ge=0, le=1000),
20+
q: Union[str, None] = None,
21+
item: Union[Item, None] = None,
2222
):
2323
results = {"item_id": item_id}
2424
if q:

docs_src/body_multiple_params/tutorial001_py310.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class Item(BaseModel):
1414
@app.put("/items/{item_id}")
1515
async def update_item(
1616
*,
17-
item_id: int = Path(..., title="The ID of the item to get", ge=0, le=1000),
17+
item_id: int = Path(title="The ID of the item to get", ge=0, le=1000),
1818
q: str | None = None,
1919
item: Item | None = None,
2020
):
Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Optional
1+
from typing import Union
22

33
from fastapi import Body, FastAPI
44
from pydantic import BaseModel
@@ -8,19 +8,17 @@
88

99
class Item(BaseModel):
1010
name: str
11-
description: Optional[str] = None
11+
description: Union[str, None] = None
1212
price: float
13-
tax: Optional[float] = None
13+
tax: Union[float, None] = None
1414

1515

1616
class User(BaseModel):
1717
username: str
18-
full_name: Optional[str] = None
18+
full_name: Union[str, None] = None
1919

2020

2121
@app.put("/items/{item_id}")
22-
async def update_item(
23-
item_id: int, item: Item, user: User, importance: int = Body(...)
24-
):
22+
async def update_item(item_id: int, item: Item, user: User, importance: int = Body()):
2523
results = {"item_id": item_id, "item": item, "user": user, "importance": importance}
2624
return results

0 commit comments

Comments
 (0)