DEV Community

Cover image for FastAPI: How to use Pydantic to declare Query Parameters
Rafael de Oliveira Marques
Rafael de Oliveira Marques

Posted on • Edited on

11

FastAPI: How to use Pydantic to declare Query Parameters

It came out about three weeks ago one of the most expected features of FastAPI. At least when we're talking about Pydantic Models + FastAPI.

Yes, I'm talking about the ability to use Pydantic Models to map your query parameters.

So in this post, I'll try to show you all you 👍 can and 👎 can't do about this subject 🙂:

💻 Mapping Query Parameters

The first thing you need to do to start mapping your query parameters with Pydantic is making sure you are using FastAPI version 0.115.0 and above.

After this, you can always go to FastAPI docs to check what is already available. Sebastián and the team members make a really, really good work on keeping do docs updated and informative ✨.

📜 A little bit of History

Let's start with some examples on how we used to map Query Parameters in FastAPI. 🤓

The simplest way to do it would be:

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def search(
    limit: int | None = 10,
    skip: int | None = 1,
    filter: str | None = None
):
    return {
        "limit": limit,
        "skip": skip,
        "filter": filter
    }
Enter fullscreen mode Exit fullscreen mode

And now you can simply call:

GET http://localhost:8000/?limit=42&skip=12&filter=banana
Enter fullscreen mode Exit fullscreen mode

But if we identified that this Query Parameters would be used in other routes, we would isolate it with something like:

from typing import Any
from fastapi import Depends, FastAPI, Query

app = FastAPI()

async def pagination_query_string(
    limit: int | None = Query(10, ge=5, le=100),
    skip: int | None = Query(1, ge=1),
    filter: str | None = Query(None)
) -> dict[str, Any]:
    return {
        "limit": limit,
        "skip": skip,
        "filter": filter
    }

@app.get("/")
async def search(q: dict[str, Any] = Depends(pagination_query_string)):
    return q
Enter fullscreen mode Exit fullscreen mode

Or since we're using Pydantic to map our models, with just a little refactoring we would get:

from fastapi import Depends, FastAPI, Query
from pydantic import BaseModel

app = FastAPI()

class PaginationQueryString(BaseModel):
    limit: int | None = 10
    skip: int | None = 1
    filter: str | None = None

async def pagination_query_string(
    limit: int | None = Query(10, ge=5, le=100),
    skip: int | None = Query(1, ge=1),
    filter: str | None = Query(None)
) -> PaginationQueryString:
    return PaginationQueryString(
        limit=limit,
        skip=skip,
        filter=filter
    )

@app.get("/")
async def search(q: PaginationQueryString = Depends(pagination_query_string)):
    return q
Enter fullscreen mode Exit fullscreen mode

⌨️ Using Pydantic to map the Query Strings

Pydantic logo

Now, if we want to get our query string, we don't need to create a function and then add it as a dependency. We can simply tell FastAPI that we want an object of type PaginationQueryString and that it's a query string:

from typing import Annotated
from fastapi import FastAPI, Query
from pydantic import BaseModel

app = FastAPI()

class PaginationQueryString(BaseModel):
    limit: int | None = 10
    skip: int | None = 1
    filter: str | None = None

@app.get("/")
async def search(q: Annotated[PaginationQueryString, Query()]):
    return q
Enter fullscreen mode Exit fullscreen mode

Easy, right? 😄

❌ Forbid extra Query Parameters

To forbid extra parameters in our query strings we need to configure our Pydantic model to do so.

We'll simply add a model_config with the option extra="forbid":

from typing import Annotated
from fastapi import FastAPI, Query
from pydantic import BaseModel, ConfigDict

app = FastAPI()

class PaginationQueryString(BaseModel):
    model_config = ConfigDict(extra="forbid")

    limit: int | None = 10
    skip: int | None = 1

@app.get("/")
async def search(q: Annotated[PaginationQueryString, Query()]):
    return q
Enter fullscreen mode Exit fullscreen mode

Note that we removed the filter parameter in our model. Now if we call:

GET http://localhost:8000/?limit=42&skip=12&filter=banana
Enter fullscreen mode Exit fullscreen mode

We'll get an error informing us that extra inputs are not allowed:

{
    "detail": [
        {
            "type": "extra_forbidden",
            "loc": [
                "query",
                "filter"
            ],
            "msg": "Extra inputs are not permitted",
            "input": "banana"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

⚠️ What are the limitations?

At least at version 0.115.0, it don't work very well with nested models.

Let's try something like:

from typing import Annotated
from fastapi import FastAPI, Query
from pydantic import BaseModel

app = FastAPI()

class Filter(BaseModel):
    name: str | None = None
    age: int | None = None
    nickname: str | None = None

class PaginationQueryString(BaseModel):
    limit: int | None = 10
    skip: int | None = 1
    filter: Filter | None = None

@app.get("/")
async def search(q: Annotated[PaginationQueryString, Query()]):
    return q
Enter fullscreen mode Exit fullscreen mode

If we call it like before:

GET http://localhost:8000/?limit=42&skip=12&filter=chocolate
Enter fullscreen mode Exit fullscreen mode

We'll get an error telling us that filter is an object:

{
    "detail": [
        {
            "type": "model_attributes_type",
            "loc": [
                "query",
                "filter"
            ],
            "msg": "Input should be a valid dictionary or object to extract fields from",
            "input": "chocolate"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

At least right now, it's absolutely right! We changed our filter to be a Pydantic model, not a string. But if we try to convert it to a dictionary:

http://localhost:8000/?limit=42&skip=12&filter={%22name%22:%20%22Rafael%22,%20%22age%22:%2038,%20%22nickname%22:%20%22ceb10n%22}
Enter fullscreen mode Exit fullscreen mode

FastAPI will tell us that filter needs to be a valid dictionary 😔:

{
    "detail": [
        {
            "type": "model_attributes_type",
            "loc": [
                "query",
                "filter"
            ],
            "msg": "Input should be a valid dictionary or object to extract fields from",
            "input": "{\"name\": \"Rafael\", \"age\": 38, \"nickname\": \"ceb10n\"}"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

It's happening this because FastAPI will rely on Starlette's QueryParams, that will give a string to FastAPI, not a dict. And at least in version 0.115.0, this will give you an error.

⁉️ So, when do I use Pydantic models with my Query Parameters?

It's quite simple:

✅ You have simple query strings that don't need any elaborate fancy nested objects? Use it! 👍

❌ You created a complex nested query string? Don't use it yet 👎. (And maybe you should try to rethink your query strings. 😇 The simpler, the better 😉)

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read more

Top comments (1)

Collapse
 
messanga11 profile image
Messanga11

If found a trick for filters:
use this type into filters instead:
filters: Optional[Union[Annotated[PaginationQueryString, Query()], str]] = None
And then override the filters value
filters = PaginationQueryString(**json.loads(filters)).model_dump()

I used a try cath to retrhow a 422 validation manually.

I am working on a generic CrudRoutes generator, this article really helped me with building the listing with pagination function. Thanks a lot..🙏🏾🙏🏾🙏🏾🙏🏾🙏🏾🙏🏾

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more