FastAPI: How to know if a parameter is really null? - python

i have a ressource and want to have a post api endpoint to modify it. My problem is if i set all propertys Optional[...] how did i know if i want to "delete" one property or set it to null? If i set it in the request to null: I get NoneType. But if i don't set it in the request i also get NoneType. Is there a solution to differ between this cases?
Here is an example program:
from typing import Optional
from fastapi import FastAPI
import uvicorn
from pydantic import BaseModel
class TestEntity(BaseModel):
first: Optional[str]
second: Optional[str]
third: Optional[str]
app = FastAPI()
#app.post("/test")
def test(entity: TestEntity):
return entity
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=5000)
I want to set first to null and don't do anything with the other propertys, I do:
{
"first":null
}
via POST request. As response I get:
{
"first": null,
"second": null,
"third": null
}
As you can see you cannot know which property is set null and which propertys should remain the same.

You can find your answer here : Pydantic: Detect if a field value is missing or given as null
#app.post("/test")
def test(entity: TestEntity):
return entity.dict(exclude_unset=True)

Related

How to document default None/null in OpenAPI/Swagger using FastAPI?

Using a ORM, I want to do a POST request letting some fields with a null value, which will be translated in the database for the default value specified there.
The problem is that OpenAPI (Swagger) docs, ignores the default None and still prompts a UUID by default.
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional
from uuid import UUID
import uvicorn
class Table(BaseModel):
# ID: Optional[UUID] # the docs show a example UUID, ok
ID: Optional[UUID] = None # the docs still shows a uuid, when it should show a null or valid None value.
app = FastAPI()
#app.post("/table/", response_model=Table)
def create_table(table: Table):
# here we call to sqlalchey orm etc.
return 'nothing important, the important thing is in the docs'
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
In the OpenAPI schema example (request body) which is at the docs we find:
{
"ID": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
This is not ok, because I specified that the default value is None,so I expected this instead:
{
"ID": null, # null is the equivalent of None here
}
Which will pass a null to the ID and finally will be parsed in the db to the default value (that is a new generated UUID).
When you declare Optional parameters, users shouldn't have to include those parameters in their request (specified with null value) in order to be null. The default value of the parameters will be null, unless the user specifies some other value when sending the request.
Hence, all you have to do is to declare a custom example for the Pydantic model using Config and schema_extra, as described in the documentation and as shown below. The below example will create an empty (i.e., {}) request body in OpenAPI (Swagger UI), which can be successfully submitted (as ID is the only attribute of the model and is optional).
class Table(BaseModel):
ID: Optional[UUID] = None
class Config:
schema_extra = {
"example": {
}
}
#app.post("/table/", response_model=Table)
def create_table(table: Table):
return table
If the Table model included some other required attributes, you could add example values for those, as demonstrated below:
class Table(BaseModel):
ID: Optional[UUID] = None
some_attr: str
class Config:
schema_extra = {
"example": {
"some_attr": "Foo"
}
}
If you would like to keep the auto-generated examples for the rest of the attributes except the one for the ID attribute, you could use the below to remove ID from the model's properties in the generated schema (inspired by Schema customization):
class Table(BaseModel):
ID: Optional[UUID] = None
some_attr: str
some_attr2: float
some_attr3: bool
class Config:
#staticmethod
def schema_extra(schema: Dict[str, Any], model: Type['Table']) -> None:
del schema.get('properties')['ID']
Also, if you would like to add custom example to some of the attributes, you could use Field() (as described here); for example, some_attr: str = Field(example="Foo").
Another possible solution would be to modify the generated OpenAPI schema, as described in Solution 3 of this answer. Though, the above solution is likely more suited to this case.
Note
ID: Optional[UUID] = None is the same as ID: UUID = None. As previously documented in FastAPI website (see this answer):
The Optional in Optional[str] is not used by FastAPI, but will allow
your editor to give you better support and detect errors.
Since then, FastAPI has revised their documentation with the following:
The Union in Union[str, None] will allow your editor to give you
better support and detect errors.
Hence, ID: Union[UUID, None] = None is the same as ID: Optional[UUID] = None and ID: UUID = None. In Python 3.10+, one could also use ID: UUID| None = None (see here).
As per FastAPI documentation (see Info section in the link provided):
Have in mind that the most important part to make a parameter optional
is the part:
= None
or the:
= Query(default=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.

OpenAPI is missing schemas for some of the Pydantic models in FastAPI app

I am building a FastAPI application, which has a lot of Pydantic models. Even though the application is working just fine, as expected the OpenAPI (Swagger UI) docs do not show the schema for all of these models under the Schemas section.
Here are the contents of pydantic schemas.py
import socket
from datetime import datetime
from enum import Enum
from typing import Any, Dict, List, Optional, Set, Union
from pydantic import BaseModel, Field, validator
from typing_extensions import Literal
ResponseData = Union[List[Any], Dict[str, Any], BaseModel]
# Not visible in Swagger UI
class PageIn(BaseModel):
page_size: int = Field(default=100, gt=0)
num_pages: int = Field(default=1, gt=0, exclude=True)
start_page: int = Field(default=1, gt=0, exclude=True)
# visible under schemas on Swagger UI
class PageOut(PageIn):
total_records: int = 0
total_pages: int = 0
current_page: int = 1
class Config: # pragma: no cover
#staticmethod
def schema_extra(schema, model) -> None:
schema.get("properties").pop("num_pages")
schema.get("properties").pop("start_page")
# Not visible in Swagger UI
class BaseResponse(BaseModel):
host_: str = Field(default_factory=socket.gethostname)
message: Optional[str]
# Not visible in Swagger UI
class APIResponse(BaseResponse):
count: int = 0
location: Optional[str]
page: Optional[PageOut]
data: ResponseData
# Not visible in Swagger UI
class ErrorResponse(BaseResponse):
error: str
# visible under schemas on Swagger UI
class BaseFaultMap(BaseModel):
detection_system: Optional[str] = Field("", example="obhc")
fault_type: Optional[str] = Field("", example="disk")
team: Optional[str] = Field("", example="dctechs")
description: Optional[str] = Field(
"",
example="Hardware raid controller disk failure found. "
"Operation can continue normally,"
"but risk of data loss exist",
)
# Not visible in Swagger UI
class FaultQueryParams(BaseModel):
f_id: Optional[int] = Field(None, description="id for the host", example=12345, title="Fault ID")
hostname: Optional[str]
status: Literal["open", "closed", "all"] = Field("open")
created_by: Optional[str]
environment: Optional[str]
team: Optional[str]
fault_type: Optional[str]
detection_system: Optional[str]
inops_filters: Optional[str] = Field(None)
date_filter: Optional[str] = Field("",)
sort_by: Optional[str] = Field("created",)
sort_order: Literal["asc", "desc"] = Field("desc")
All of these models are actually being used in FastAPI paths to validate the request body. The FaultQueryParams is a custom model, which I use to validate the request query params and is used like below:
query_args: FaultQueryParams = Depends()
The rest of the models are being used in conjunction with Body field. I am not able to figure out why only some of the models are not visible in the Schemas section while others are.
Also another thing I noticed about FaultQueryParams is that the description, examples do not show up against the path endpoint even though they are defined in the model.
Edit 1:
I looked more into and realized that all of the models which are not visible in swagger UI are the ones that are not being used directly in path operations i.e., these models are not being used as response_model or Body types and are sort of helper models which are being used indirectly. So, it seems like FastAPI is not generating the schema for these models.
One exception to the above statement is query_args: FaultQueryParams = Depends() which is being used directly in a path operation to map the Query params for the endpoint against a custom model. This is a problem because swagger is not identifying the meta parameters like title, description, example from the fields of this model & not showing on the UI which is important for the users of this endpoint.
Is there a way to trick FastAPI to generate schema for the custom model FaultQueryParams just like it generates for Body, Query etc ?
FastAPI will generate schemas for models that are used either as a Request Body or Response Model. When declaring query_args: FaultQueryParams = Depends() (using Depends), your endpoint would not expect a request body, but rather query parameters; hence, FaultQueryParams would not be included in the schemas of the OpenAPI docs.
To add additional schemas, you could extend/modify the OpenAPI schema. Example is given below (make sure to add the code for modifying the schema after all routes have been defined, i.e., at the end of your code).
class FaultQueryParams(BaseModel):
f_id: Optional[int] = Field(None, description="id for the host", example=12345, title="Fault ID")
hostname: Optional[str]
status: Literal["open", "closed", "all"] = Field("open")
...
#app.post('/predict')
def predict(query_args: FaultQueryParams = Depends()):
return query_args
def get_extra_schemas():
return {
"FaultQueryParams": {
"title": "FaultQueryParams",
"type": "object",
"properties": {
"f_id": {
"title": "Fault ID",
"type": "integer",
"description": "id for the host",
"example": 12345
},
"hostname": {
"title": "Hostname",
"type": "string"
},
"status": {
"title": "Status",
"enum": [
"open",
"closed",
"all"
],
"type": "string",
"default": "open"
},
...
}
}
}
from fastapi.openapi.utils import get_openapi
def custom_openapi():
if app.openapi_schema:
return app.openapi_schema
openapi_schema = get_openapi(
title="FastAPI",
version="1.0.0",
description="This is a custom OpenAPI schema",
routes=app.routes,
)
new_schemas = openapi_schema["components"]["schemas"]
new_schemas.update(get_extra_schemas())
openapi_schema["components"]["schemas"] = new_schemas
app.openapi_schema = openapi_schema
return app.openapi_schema
app.openapi = custom_openapi
Some Helpful Notes
Note 1
Instead of manually typing the schema for the extra models that you would like to add to the docs, you can have FastAPI do that for you by adding to your code an endpoint (which you would subsequently remove, after getting the schema) using that model as a request body or response model, for example:
#app.post('/predict')
def predict(query_args: FaultQueryParams):
return query_args
Then, you can get the generated JSON schema at http://127.0.0.1:8000/openapi.json, as described in the documentation. From there, you can either copy and paste the schema of the model to your code and use it directly (as shown in the get_extra_schema() method above) or save it to a file and load the JSON data from the file, as demonstrated below:
import json
...
new_schemas = openapi_schema["components"]["schemas"]
with open('extra_schemas.json') as f:
extra_schemas = json.load(f)
new_schemas.update(extra_schemas)
openapi_schema["components"]["schemas"] = new_schemas
...
Note 2
To declare metadata, such as description, example, etc, for your query parameter, you should define your parameter with Query instead of Field, and since you can't do that with Pydantic models, you could declare a custom dependency class, as decribed here and as shown below:
from fastapi import FastAPI, Query, Depends
from typing import Optional
class FaultQueryParams:
def __init__(
self,
f_id: Optional[int] = Query(None, description="id for the host", example=12345)
):
self.f_id = f_id
app = FastAPI()
#app.post('/predict')
def predict(query_args: FaultQueryParams = Depends()):
return query_args
The above can be re-written using the #dataclass decorator, as shown below:
from fastapi import FastAPI, Query, Depends
from typing import Optional
from dataclasses import dataclass
#dataclass
class FaultQueryParams:
f_id: Optional[int] = Query(None, description="id for the host", example=12345)
app = FastAPI()
#app.post('/predict')
def predict(query_args: FaultQueryParams = Depends()):
return query_args
Thank to #Chris for the pointers which ultimately led me to use dataclasses for defining query params in bulk and it just worked fine.
#dataclass
class FaultQueryParams1:
f_id: Optional[int] = Query(None, description="id for the host", example=55555)
hostname: Optional[str] = Query(None, example="test-host1.domain.com")
status: Literal["open", "closed", "all"] = Query(
None, description="fetch open/closed or all records", example="all"
)
created_by: Optional[str] = Query(
None,
description="fetch records created by particular user",
example="user-id",
)

Is it possible to get query params with Pydantic if param's name use special symbols?

I'm handling this request in my code (Python3.9, FastAPI, Pydantic):
https://myapi.com/api?params[A]=1&params[B]=2
I tried to make following model:
BaseModel for handling special get request
(for fastapi.Query and pydantic.Field is same)
I also set up aliases for it, but in swagger docs I see next field:
Snap of the swagger docs
There are fields that are specified as extra_data
So, if I specify query params in parameters of my endpoint like this:
#app.get('/')
def my_handler(a: str = Query(None, alias="params[A]")):
return None
Everything works fine. How can I fix it? I want to initialize my pydantic.BaseModel with speacial aliases using this way and avoid usage of query-params in
class MyModel(BaseModel):
a = Field(alias="params[A]")
b = Field(alias="params[B]")
def my_handler(model: MyModel = Depends()):
return model.dict()

How to disable schema checking in FastAPI?

I am migrating a service from Flask to FastAPI and using Pydantic models to generate the documentation. However, I'm a little unsure about the schema check. I'm afraid there will be some unexpected data (like a different field format) and it will return an error.
In the Pydantic documentation there are ways to create a model without checking the schema: https://pydantic-docs.helpmanual.io/usage/models/#creating-models-without-validation
However, as this is apparently instantiated by FastAPI itself, I don't know how to disable this schema check when returning from FastAPI.
You could return Response directly, or with using custom responses for automatic convertion. In this case, response data is not validated against the response model. But you can still document it as described in Additional Responses in OpenAPI.
Example:
class SomeModel(BaseModel):
num: int
#app.get("/get", response_model=SomeModel)
def handler(param: int):
if param == 1: # ok
return {"num": "1"}
elif param == 2: # validation error
return {"num": "not a number"}
elif param == 3: # ok (return without validation)
return JSONResponse(content={"num": "not a number"})
elif param == 4: # ok (return without validation and conversion)
return Response(content=json.dumps({"num": "not a number"}), media_type="application/json")
You can set the request model as a typing.Dict or typing.List
from typing import Dict
app.post('/')
async def your_function(body: Dict):
return { 'request_body': body}
FastAPI doesn't enforce any kind of validation, so if you don't want it, simply do not use Pydantic models or type hints.
app.get('/')
async def your_function(input_param):
return { 'param': input_param }
# Don't use models or type hints when defining the function params.
# `input_param` can be anything, no validation will be performed.
However, as #Tryph rightly pointed out, since you're using Pydantic to generate the documentation, you could simply use the Any type like so:
from typing import Any
from pydantic import BaseModel
class YourClass(BaseModel):
any_value: Any
Beware that the Any type also accepts None, making in fact the field optional. (See also typing.Any in the Pydantic docs)

How to generate response decriptions in FastAPI

I want to generate a description of all available responses (along with code 200 example), which are represented in the code, like here.
from typing import Any
import uvicorn
from fastapi import FastAPI, HTTPException
router = FastAPI()
from pydantic import BaseModel
class FileItemBase(BaseModel):
current_project: str = "Test project"
class FileItemInDBBase(FileItemBase):
id: int
folder_path: str
class Config:
orm_mode = True
class FileResponse(FileItemInDBBase):
pass
#router.get("/", response_model=FileResponse)
def example_code() -> Any:
"""
# beautiful description
to demonstrate functionality
"""
demo=True
if demo:
raise HTTPException(418, "That is a teapot.")
if __name__ =="__main__":
uvicorn.run(router)
What I got with this is such a description.
When I try this out - I got an error response (as expected).
What I want - is the description of an error included in the example responses, like here. A Frontend-developer can look at this description and process such cases in the right way without testing the API.
I know how it can be made within OpenAPI specs.
Is there a way to generate this description with FastAPI?
You can add a responses parameter to your path operation.
Then you can pass your model there. It will create a schema for that model.
class FileItemBase(BaseModel):
current_project: str = "Test project"
#app.get("/", response_model=FileItemBase, responses={418: {"model": FileItemBase}})
def example_code():
"""
# beautiful description
to demonstrate functionality
"""
demo = True
if demo:
raise HTTPException(418, "That is a teapot.")

Categories

Resources