FastAPI Interactive Docs

Query Parameter Models

Bundling query params, the neat way

Last time we put guardrails on path and query values with validations. Those Query() calls did the job, but once you have five or six filters hanging off one endpoint, the function signature starts looking like a grocery receipt. 🧾

Good news: since FastAPI 0.115.0 you can group related query parameters into a single Pydantic model. One model, reused anywhere, validated all at once.

The old way (still fine, just crowded)

One Query() per filter, all stacked in the signature:

from typing import Optionalfrom fastapi import FastAPI, Queryapp = FastAPI()@app.get("/blogs")async def read_blogs(    limit: int = Query(10, gt=0, le=100),    offset: int = Query(0, ge=0),    author: Optional[str] = None,):    return {"limit": limit, "offset": offset, "author": author}

Let's review:

  1. limit is capped between 1 and 100 with gt and le.

  2. offset cannot go negative thanks to ge=0.

  3. author is optional, so leaving it out is fine.

Works great. But imagine reusing these three filters across ten endpoints. You would copy-paste this every single time. No thanks.

The model way

Move the filters into a Pydantic model, then declare the parameter as Query():

from typing import Annotated, Optionalfrom fastapi import FastAPI, Queryfrom pydantic import BaseModel, Fieldapp = FastAPI()class BlogFilter(BaseModel):    limit: int = Field(10, gt=0, le=100)    offset: int = Field(0, ge=0)    author: Optional[str] = None@app.get("/blogs")async def read_blogs(filter_query: Annotated[BlogFilter, Query()]):    return filter_query

Let's review:

  1. BlogFilter holds all three filters in one place, validations included via Field.

  2. Annotated[BlogFilter, Query()] tells FastAPI to pull each field from the query string, not the request body.

  3. FastAPI hands you back a ready BlogFilter object. Hit /blogs?limit=5&author=Alice and it just works.

Same validation, way less clutter. Reuse BlogFilter on any endpoint that needs the same filters.

Locking the door on extra params

By default FastAPI ignores query params it does not recognize. If you would rather reject anything unexpected, tell Pydantic to forbid extras:

class BlogFilter(BaseModel):    model_config = {"extra": "forbid"}    limit: int = Field(10, gt=0, le=100)    offset: int = Field(0, ge=0)    author: Optional[str] = None

Let's review:

  1. model_config = {"extra": "forbid"} flips on strict mode.

  2. Now /blogs?limit=5&random=oops returns a 422 instead of quietly ignoring random.

  3. Handy when you want clients to notice typos instead of wondering why a filter did nothing.

Practice Lab:

A Pydantic model on a query parameter only behaves like query params because of Query(). Drop the Query() and FastAPI assumes you meant a request body, so your nice ?limit=5 stops binding. Keep that annotation in.

⏭️ Next up: Body - Multiple Parameters, where we stop bundling everything into one model and let a single endpoint take several body params at once. See you there.