Parse JSON-encoded query strings in FastAPI
React ↔︎ FastAPI dashboard
I work on a dashboard that shows charts with filters.
- React serializes filter parameters into JSON and sends them to the FastAPI in a query string.
- The FastAPI server returns the data to display the chart.
FastAPI parses your request and maps it to controller parameters. Can I turn complex parameters in the query string into Pydantic models?
class Quarter(BaseModel):
q: int
year: int
@app.get("/api/v1/chart")
def get_chart(
start_quarter: Quarter = parse_querystring_and_create_quarter_instance_somehow(),
end_quarter: Quarter = parse_querystring_and_create_quarter_instance_somehow(),
):
...
FastAPI request mapping
FastAPI automatically maps query strings to scalar function parameters. We get strings inside the functions.
@app.get("/api/v1/chart")
def get_chart(start_quarter: str, end_quarter: str):
...
Ref: Query Parameters in the FastAPI documentation.
Below is the same example, with a bit more verbose mapping. There is no difference for FastAPI, but a clearer message for the readers of our code. “Hey, reader, the start_quarter
and end_quarter
parameters are taken from the query string,” it says.
from fastapi import Query
@app.get("/api/v1/chart")
def get_chart(start_quarter: str = Query(), end_quarter: str = Query()):
...
It’s a bit less well-known, but FastAPI can accept JSON-encoded values and turn them into dicts.
from fastapi import Query, Json
@app.get("/api/v1/chart")
def get_chart(start_quarter: Json = Query(), end_quarter: Json = Query()):
...
In the code above, both start_quarter
and end_quarter
will be passed to the function as dicts. Inside the function, we can use these dicts as-is or call BaseModel.parse_obj, or even better, pydantic.parse_obj_as to convert dicts to model instances.
@app.get("/api/v1/chart")
def get_chart(start_quarter: Json = Query(), end_quarter: Json = Query()):
start_quarter_obj = Quarter.parse_obj(start_quarter)
end_quarter_obj = Quarter.parse_obj(end_quarter)
...
I prefer pydantic.parse_obj_as()
over BaseModel.parse_obj()
because the former can accept more complex types for parsing. For example, you can use parse_obj_as(Quarter | None, start_quarter)
for optional arguments.
Models in query string parameters?
I thought defining a model and passing it as a query would be enough. Will the following example work?
from fastapi import Query, Json
from pydantic import BaseModel
class Quarter(BaseModel):
q: int
year: int
@app.get("/api/v1/chart")
def get_chart(start_quarter: Quarter = Query(), end_quarter: Quarter = Query()):
...
Well, it doesn’t. The FastAPI server doesn’t accept models in query strings, and an attempt to run this code throws back an assertion error.
AssertionError: Param: start_quarter can only be a request body, using Body()
Workarounds
There is a feature request #884 asking for the support of this feature and enumerating workarounds. Sebastián Ramírez, the author of FastAPI, closed the issue, clearly stating that the server will not support this. Therefore, the workaround is our only solution.
The simplest workaround is to go with dependencies and create functions turning raw JSON objects into models.
from fastapi import Depends
from pydantic import parse_obj_as
def get_start_quarter(start_quarter: Json = Query()) -> Quarter:
return parse_obj_as(Quarter, start_quarter)
def get_end_quarter(end_quarter: Json = Query()) -> Quarter:
return parse_obj_as(Quarter, end_quarter)
@app.get("/api/v1/chart")
def get_chart(
start_quarter: Quarter = Depends(get_start_quarter),
end_quarter: Quarter = Depends(get_end_quarter),
):
...
This code works well, but we must create a separate function for every query string parameter.
Generalized solution
I created a json_param()
function that’s used in place of Depends()
to let FastAPI know that it should convert the parameter from a JSON-encoded query string to a model.
from typing import Type
from fastapi import Depends, HTTPException, Query
from pydantic import BaseModel, Json, ValidationError
def json_param(param_name: str, model: Type[BaseModel], **query_kwargs):
"""Parse JSON-encoded query parameters as pydantic models.
The function returns a `Depends()` instance that takes the JSON-encoded value from
the query parameter `param_name` and converts it to a Pydantic model, defined
by the `model` attribute.
"""
def get_parsed_object(value: Json = Query(alias=param_name, **query_kwargs)):
try:
return pydantic.parse_obj_as(model, value)
except ValidationError as err:
raise HTTPException(400, detail=err.errors())
return Depends(get_parsed_object)
Usage example.
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class User(BaseModel):
name: str
@app.get("/")
def root(user: User = json_param("user", User, description="User object")):
return {"message": f"Hello, {user!r}"}
Request and response examples (with httpie)
Success:
$ http localhost:7000 user=='{"name": "Foo"}'
HTTP/1.1 200 OK
{
"message": "Hello, User(name='Foo')"
}
Validation error:
$ http localhost:7000 user=='{"name": null}'
HTTP/1.1 400 Bad Request
{
"detail": [
{
"loc": [
"name"
],
"msg": "none is not an allowed value",
"type": "type_error.none.not_allowed"
}
]
}
The gist snippet is available at https://gist.github.com/imankulov/cef71dd5a01f9a27caeb66f7bedaf241.