Roman Imankulov

Roman Imankulov

Full-stack Python web developer from Porto

search results (esc to close)
14 Jun 2022

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.
My dashboard

My dashboard

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.

Roman Imankulov

Hey, I am Roman, and you can hire me.

I am a full-stack Python web developer who loves helping startups and small teams turn their ideas into products.

More about me and my skills