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

Question:

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?

Asked By: NineWasps

||

Answers:

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

Answered By: funnydman

Pydantic provides the following arguments for exporting models using the model.dict(...) method:

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.dict(exclude_none=True))
#> {'data': [{'id': '1', 'text': 'The text 1'}]}
print(t.dict(exclude_unset=True))
#> {'data': [{'id': '1', 'text': 'The text 1'}], 'n_processes': None}

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
Answered By: Chris
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.