How to exclude Optional unset values from a Pydantic model using FastAPI?
Asked Answered
Y

3

10

I have this model:

class Text(BaseModel):
    id: str
    text: str = None


class TextsRequest(BaseModel):
    data: list[Text]
    n_processes: Union[int, None]

So I want to be able to take requests like:

{"data": ["id": "1", "text": "The text 1"], "n_processes": 8} 

and

{"data": ["id": "1", "text": "The text 1"]}.

Right now in the second case I get

{'data': [{'id': '1', 'text': 'The text 1'}], 'n_processes': None}

using this code:

app = FastAPI()

@app.post("/make_post/", response_model_exclude_none=True)
async def create_graph(request: TextsRequest):
    input_data = jsonable_encoder(request)

So how can I exclude n_processes here?

Yaupon answered 1/8, 2022 at 19:58 Comment(0)
T
7
Since Pydantic >= 2.0 deprecates model.dict() use model.model_dump(...) instead.

You can use exclude_none param of Pydantic's model.dict(...):

class Text(BaseModel):
    id: str
    text: str = None


class TextsRequest(BaseModel):
    data: list[Text]
    n_processes: Optional[int]


request = TextsRequest(**{"data": [{"id": "1", "text": "The text 1"}]})
print(request.dict(exclude_none=True))

Output:

{'data': [{'id': '1', 'text': 'The text 1'}]}

Also, it's more idiomatic to write Optional[int] instead of Union[int, None].

Talos answered 1/8, 2022 at 20:39 Comment(2)
Would disagree with your last statement. Union is closer to python 3.10 syntax than Optional, but that’s just my opinion.Dapplegray
Pydantic >= 2.0 deprecates model.dict(). As Eapen Jose wrote, use model_dump().Mckenna
E
7

Pydantic provides the following arguments for exporting models using the model.dict(...) method. Update: the model.dict(...) was deprecated (but still supported) and replaced by model.model_dump(...). Those parameters are as follows:

exclude_unset: whether fields which were not explicitly set when creating the model should be excluded from the returned dictionary; default False

exclude_none: whether fields which are equal to None should be excluded from the returned dictionary; default False

Since you are refering to excluding optional unset parameters, you can use the first method (i.e., exclude_unset). This is useful when one would like to exclude a parameter only if it has not been set to either some value or None.

The exclude_none argument, however, ignores that fact that an attribute may have been intentionally set to None, and hence, excludes it from the returned dictionary.

Example:

from pydantic import BaseModel
from typing import List, Union

class Text(BaseModel):
    id: str
    text: str = None

class TextsRequest(BaseModel):
    data: List[Text]    # in Python 3.9+ you can use:  data: list[Text]
    n_processes: Union[int, None] = None

t = TextsRequest(**{'data': [{'id': '1', 'text': 'The text 1'}], 'n_processes': None})
print(t.model_dump(exclude_none=True))
#> {'data': [{'id': '1', 'text': 'The text 1'}]}
print(t.model_dump(exclude_unset=True))
#> {'data': [{'id': '1', 'text': 'The text 1'}], 'n_processes': None}

Excluding Unset or None parameters from the response

If what you needed is excluding Unset or None parameters from the endpoint's response, without necessarily calling model.dict(...) (or, in Pydantic V2 model.model_dump(...)) inside the endpoint on your own, you could instead use the endpoint's decorator parameter response_model_exclude_unset or response_model_exclude_none (see the relevant documentation and related answers here and here). To achieve that, FastAPI, behind the scenes, uses the aforementioned model.dict(...) (or, in Pydantic V2 model.model_dump(...)) method with its exclude_unset or exclude_none parameter.

Examples:

@app.post("/create", response_model_exclude_unset=True)
async def create_graph(t: TextsRequest):
    return t

or

@app.post("/create", response_model_exclude_none=True)
async def create_graph(t: TextsRequest):
    return t

About Optional Parameters

Using Union[int, None] is the same as using Optional[int] (both are equivalent). The most important part, however, to make a parameter optional is the part = None.

As per FastAPI documentation (see admonition Note and Info in the link provided):

Note

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

The Union in Union[str, None] will allow your editor to give you better support and detect errors.

Info

Have in mind that the most important part to make a parameter optional is the part: = None, as it will use that None as the default value, and that way make the parameter 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.

Hence, regardless of the option you may choose to use, if it is not followed by the = None part, FastAPI won't know that the value of the parameter is optional, and hence, the user will have to provide some value for it. One can also check that through the auto-generated API docs at http://127.0.0.1:8000/docs, where the parameter or request body will appear as a Required field.

For example, any of the below would require the user to pass some body content in their request for the TextsRequest model:

@app.post("/upload")
def upload(t: Union[TextsRequest, None]):
    pass

@app.post("/upload")
def upload(t: Optional[TextsRequest]):
    pass

If, however, the above TextsRequest definitions were succeeded by = None, for example:

@app.post("/upload")
def upload(t: Union[TextsRequest, None] = None):
    pass

@app.post("/upload")
def upload(t: Optional[TextsRequest] = None):
    pass
    
@app.post("/upload")
def upload(t: TextsRequest = None): # this should work as well
    pass

the parameter (or body) would be optional, as = None would tell FastAPI that this parameter is not required.

In Python 3.10+

The good news is that in Python 3.10 and above, you don't have to worry about names like Optional and Union, as you can simply use the vertical bar | (also called bitwise or operator, but that meaning is not relevant here) to define an optional parameter (or simply, unions of types). However, the same rule applies to this option as well, i.e., you would still need to add the = None part, if you would like to make the parameter optional (as demonstrated in the example given below).

Example:

@app.post("/upload")
def upload(t: TextsRequest | None = None):
    pass
Embosser answered 6/8, 2022 at 16:22 Comment(2)
The FastAPI link and Pydantic link are both deadTriceratops
Thanks for letting me know. They have been updated.Embosser
F
1

Instead of response_model_exlclude_none you can use response_model_exclude_unset

app = FastAPI()

@app.post("/make_post/", response_model_exclude_unset=True)
async def create_graph(request: TextsRequest):
    input_data = jsonable_encoder(request)
Frankie answered 6/7, 2023 at 6:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.