python 130 lines · 3 tabs

Validate and Normalize Query Filters With a Pydantic Dependency in FastAPI

Shared by codesnips Aug 2026
3 tabs
from enum import Enum
from typing import Optional

from fastapi import Query
from pydantic import BaseModel, Field, field_validator


class SortField(str, Enum):
    name = "name"
    price = "price"
    created_at = "created_at"


class SortDir(str, Enum):
    asc = "asc"
    desc = "desc"


class ProductFilters(BaseModel):
    search: Optional[str] = Field(Query(None, description="Free-text name search"))
    category: Optional[str] = Field(Query(None))
    in_stock: Optional[bool] = Field(Query(None))
    sort: SortField = Field(Query(SortField.created_at))
    order: SortDir = Field(Query(SortDir.desc))
    page: int = Field(Query(1, ge=1))
    page_size: int = Field(Query(20, ge=1, le=100))

    @field_validator("search", "category")
    @classmethod
    def normalize_text(cls, value):
        if value is None:
            return None
        cleaned = " ".join(value.split())
        return cleaned or None

    @property
    def offset(self) -> int:
        return (self.page - 1) * self.page_size

    @classmethod
    def as_dependency(
        cls,
        search: Optional[str] = Query(None),
        category: Optional[str] = Query(None),
        in_stock: Optional[bool] = Query(None),
        sort: SortField = Query(SortField.created_at),
        order: SortDir = Query(SortDir.desc),
        page: int = Query(1, ge=1),
        page_size: int = Query(20, ge=1, le=100),
    ) -> "ProductFilters":
        return cls(
            search=search,
            category=category,
            in_stock=in_stock,
            sort=sort,
            order=order,
            page=page,
            page_size=page_size,
        )
3 files · python Explain with highlit

Endpoints that list resources tend to accumulate a mess of loose query parameters: pagination, sorting, status filters, free-text search. Threading each one through the function signature and re-validating it by hand is repetitive and error-prone. This snippet centralizes all of that into a single Pydantic model that FastAPI injects as a dependency, so the endpoint receives one validated, normalized object instead of a dozen raw strings.

In filters.py, ProductFilters is a plain BaseModel whose fields map to query parameters. Each field uses Field(Query(...)) so FastAPI still documents them individually in OpenAPI and parses them from the query string, while Pydantic enforces bounds — page must be >= 1, page_size is clamped between 1 and 100. The SortField and SortDir enums restrict sort and order to known values, which turns a whole class of typos into automatic 422 responses. A field_validator on search trims whitespace and collapses empty strings to None, so downstream code never has to distinguish '' from a missing filter. The computed offset property derives the SQL offset from page and page_size, keeping that arithmetic in one place.

The key move is the as_dependency classmethod. FastAPI can use a class directly as a dependency, but wrapping it lets the model's own constructor run against the parsed query values, giving a clean typed instance rather than relying on implicit binding. Because it is a normal callable returning ProductFilters, it composes with Depends like any other provider.

In repository.py, ProductRepository.list accepts that validated object and does nothing but build a query — apply_search, ordering via getattr on the model column, and limit/offset from the already-safe values. No defensive checks are needed here because invalid input never reaches this layer.

In router.py, the endpoint signature is just filters: ProductFilters = Depends(ProductFilters.as_dependency). The handler stays tiny: it delegates to the repository and returns a PagedResponse echoing the normalized page and page_size. The trade-off is a small amount of indirection in exchange for one authoritative definition of what a valid filter set looks like, reusable across every list endpoint and fully reflected in the generated docs.


Related snips

Share this code

Here's the card — post it anywhere.

Validate and Normalize Query Filters With a Pydantic Dependency in FastAPI — share card
Link copied