Roman Imankulov

Roman Imankulov

Full-stack Python web developer

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 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 slightly more verbose mapping. There is no difference for FastAPI, but it sends a clearer message to readers of our code: “Hey, reader, the start_quarter and end_quarter parameters are taken from the query string.”

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) with 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 an assertion error.

AssertionError: Param: start_quarter can only be a request body, using Body()

Workarounds

There is a feature request #884 asking for 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, a workaround is our only solution.

The simplest workaround is to use dependencies and create functions that turn 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 can be 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

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