Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add support for not needing ... as default value in required Query(), Path(), Header(), etc. #4906

Merged
merged 4 commits into from May 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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