Skip to content

Commit

Permalink
✨ Add support for not needing ... as default value in required Quer…
Browse files Browse the repository at this point in the history
…y(), Path(), Header(), etc. (tiangolo#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
  • Loading branch information
tiangolo authored and JeanArhancet committed Aug 20, 2022
1 parent 75d29e5 commit b9ac508
Show file tree
Hide file tree
Showing 107 changed files with 404 additions and 314 deletions.
78 changes: 60 additions & 18 deletions docs/en/docs/tutorial/query-params-str-validations.md
Expand Up @@ -16,12 +16,12 @@ Let's take this application as example:
{!> ../../../docs_src/query_params_str_validations/tutorial001_py310.py!}
```

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

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

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

## Additional validation

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

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

So:

```Python
q: Optional[str] = Query(None)
q: Union[str, None] = Query(default=None)
```

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

```Python
q: Optional[str] = None
q: Union[str, None] = None
```

And in Python 3.10 and above:

```Python
q: str | None = Query(None)
q: str | None = Query(default=None)
```

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

```Python
= Query(None)
= Query(default=None)
```

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

The `Optional` part allows your editor to provide better support, but it is not what tells FastAPI that this parameter is not required.
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.

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

```Python
q: str = Query(None, max_length=50)
q: Union[str, None] = Query(default=None, max_length=50)
```

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*.
Expand All @@ -118,7 +118,7 @@ You can also add a parameter `min_length`:

=== "Python 3.6 and above"

```Python hl_lines="9"
```Python hl_lines="10"
{!> ../../../docs_src/query_params_str_validations/tutorial003.py!}
```

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

=== "Python 3.6 and above"

```Python hl_lines="10"
```Python hl_lines="11"
{!> ../../../docs_src/query_params_str_validations/tutorial004.py!}
```

=== "Python 3.10 and above"

```Python hl_lines="8"
```Python hl_lines="9"
{!> ../../../docs_src/query_params_str_validations/tutorial004_py310.py!}
```

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

## Default values

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

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"`:

Expand All @@ -178,26 +178,68 @@ q: str
instead of:

```Python
q: Optional[str] = None
q: Union[str, None] = None
```

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

```Python
q: Optional[str] = Query(None, min_length=3)
q: Union[str, None] = Query(default=None, min_length=3)
```

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

```Python hl_lines="7"
{!../../../docs_src/query_params_str_validations/tutorial006.py!}
```

### Required with Ellipsis (`...`)

There's an alternative way to explicitly declare that a value is required. You can set the `default` parameter to the literal value `...`:

```Python hl_lines="7"
{!../../../docs_src/query_params_str_validations/tutorial006b.py!}
```

!!! info
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>.

It is used by Pydantic and FastAPI to explicitly declare that a value is required.

This will let **FastAPI** know that this parameter is required.

### Required with `None`

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

To do that, you can declare that `None` is a valid type but still use `default=...`:

=== "Python 3.6 and above"

```Python hl_lines="8"
{!> ../../../docs_src/query_params_str_validations/tutorial006c.py!}
```

=== "Python 3.10 and above"

```Python hl_lines="7"
{!> ../../../docs_src/query_params_str_validations/tutorial006c_py310.py!}
```

!!! tip
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>.

### Use Pydantic's `Required` instead of Ellipsis (`...`)

If you feel uncomfortable using `...`, you can also import and use `Required` from Pydantic:

```Python hl_lines="2 8"
{!../../../docs_src/query_params_str_validations/tutorial006d.py!}
```

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

## Query parameter list / multiple values

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.
Expand Down Expand Up @@ -315,7 +357,7 @@ You can add a `title`:

=== "Python 3.10 and above"

```Python hl_lines="7"
```Python hl_lines="8"
{!> ../../../docs_src/query_params_str_validations/tutorial007_py310.py!}
```

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

=== "Python 3.10 and above"

```Python hl_lines="7"
```Python hl_lines="8"
{!> ../../../docs_src/query_params_str_validations/tutorial014_py310.py!}
```

Expand Down
6 changes: 4 additions & 2 deletions docs_src/additional_status_codes/tutorial001.py
@@ -1,4 +1,4 @@
from typing import Optional
from typing import Union

from fastapi import Body, FastAPI, status
from fastapi.responses import JSONResponse
Expand All @@ -10,7 +10,9 @@

@app.put("/items/{item_id}")
async def upsert_item(
item_id: str, name: Optional[str] = Body(None), size: Optional[int] = Body(None)
item_id: str,
name: Union[str, None] = Body(default=None),
size: Union[int, None] = Body(default=None),
):
if item_id in items:
item = items[item_id]
Expand Down
8 changes: 4 additions & 4 deletions docs_src/app_testing/app_b/main.py
@@ -1,4 +1,4 @@
from typing import Optional
from typing import Union

from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel
Expand All @@ -16,11 +16,11 @@
class Item(BaseModel):
id: str
title: str
description: Optional[str] = None
description: Union[str, None] = None


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


@app.post("/items/", response_model=Item)
async def create_item(item: Item, x_token: str = Header(...)):
async def create_item(item: Item, x_token: str = Header()):
if x_token != fake_secret_token:
raise HTTPException(status_code=400, detail="Invalid X-Token header")
if item.id in fake_db:
Expand Down
4 changes: 2 additions & 2 deletions docs_src/app_testing/app_b_py310/main.py
Expand Up @@ -18,7 +18,7 @@ class Item(BaseModel):


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


@app.post("/items/", response_model=Item)
async def create_item(item: Item, x_token: str = Header(...)):
async def create_item(item: Item, x_token: str = Header()):
if x_token != fake_secret_token:
raise HTTPException(status_code=400, detail="Invalid X-Token header")
if item.id in fake_db:
Expand Down
2 changes: 1 addition & 1 deletion docs_src/bigger_applications/app/dependencies.py
@@ -1,7 +1,7 @@
from fastapi import Header, HTTPException


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

Expand Down
12 changes: 6 additions & 6 deletions docs_src/body_fields/tutorial001.py
@@ -1,4 +1,4 @@
from typing import Optional
from typing import Union

from fastapi import Body, FastAPI
from pydantic import BaseModel, Field
Expand All @@ -8,14 +8,14 @@

class Item(BaseModel):
name: str
description: Optional[str] = Field(
None, title="The description of the item", max_length=300
description: Union[str, None] = Field(
default=None, title="The description of the item", max_length=300
)
price: float = Field(..., gt=0, description="The price must be greater than zero")
tax: Optional[float] = None
price: float = Field(gt=0, description="The price must be greater than zero")
tax: Union[float, None] = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item = Body(..., embed=True)):
async def update_item(item_id: int, item: Item = Body(embed=True)):
results = {"item_id": item_id, "item": item}
return results
6 changes: 3 additions & 3 deletions docs_src/body_fields/tutorial001_py310.py
Expand Up @@ -7,13 +7,13 @@
class Item(BaseModel):
name: str
description: str | None = Field(
None, title="The description of the item", max_length=300
default=None, title="The description of the item", max_length=300
)
price: float = Field(..., gt=0, description="The price must be greater than zero")
price: float = Field(gt=0, description="The price must be greater than zero")
tax: float | None = None


@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item = Body(..., embed=True)):
async def update_item(item_id: int, item: Item = Body(embed=True)):
results = {"item_id": item_id, "item": item}
return results
12 changes: 6 additions & 6 deletions docs_src/body_multiple_params/tutorial001.py
@@ -1,4 +1,4 @@
from typing import Optional
from typing import Union

from fastapi import FastAPI, Path
from pydantic import BaseModel
Expand All @@ -8,17 +8,17 @@

class Item(BaseModel):
name: str
description: Optional[str] = None
description: Union[str, None] = None
price: float
tax: Optional[float] = None
tax: Union[float, None] = None


@app.put("/items/{item_id}")
async def update_item(
*,
item_id: int = Path(..., title="The ID of the item to get", ge=0, le=1000),
q: Optional[str] = None,
item: Optional[Item] = None,
item_id: int = Path(title="The ID of the item to get", ge=0, le=1000),
q: Union[str, None] = None,
item: Union[Item, None] = None,
):
results = {"item_id": item_id}
if q:
Expand Down
2 changes: 1 addition & 1 deletion docs_src/body_multiple_params/tutorial001_py310.py
Expand Up @@ -14,7 +14,7 @@ class Item(BaseModel):
@app.put("/items/{item_id}")
async def update_item(
*,
item_id: int = Path(..., title="The ID of the item to get", ge=0, le=1000),
item_id: int = Path(title="The ID of the item to get", ge=0, le=1000),
q: str | None = None,
item: Item | None = None,
):
Expand Down
12 changes: 5 additions & 7 deletions docs_src/body_multiple_params/tutorial003.py
@@ -1,4 +1,4 @@
from typing import Optional
from typing import Union

from fastapi import Body, FastAPI
from pydantic import BaseModel
Expand All @@ -8,19 +8,17 @@

class Item(BaseModel):
name: str
description: Optional[str] = None
description: Union[str, None] = None
price: float
tax: Optional[float] = None
tax: Union[float, None] = None


class User(BaseModel):
username: str
full_name: Optional[str] = None
full_name: Union[str, None] = None


@app.put("/items/{item_id}")
async def update_item(
item_id: int, item: Item, user: User, importance: int = Body(...)
):
async def update_item(item_id: int, item: Item, user: User, importance: int = Body()):
results = {"item_id": item_id, "item": item, "user": user, "importance": importance}
return results
4 changes: 1 addition & 3 deletions docs_src/body_multiple_params/tutorial003_py310.py
Expand Up @@ -17,8 +17,6 @@ class User(BaseModel):


@app.put("/items/{item_id}")
async def update_item(
item_id: int, item: Item, user: User, importance: int = Body(...)
):
async def update_item(item_id: int, item: Item, user: User, importance: int = Body()):
results = {"item_id": item_id, "item": item, "user": user, "importance": importance}
return results

0 comments on commit b9ac508

Please sign in to comment.