Skip to content

FastAPI Filter

Add querystring filters to your api endpoints and show them in the swagger UI.

The supported backends are SQLAlchemy and MongoEngine.

Example

Swagger UI

Filter

The philosophy of fastapi_filter is to be very declarative. You define the fields you want to be able to filter on as well as the type of operator, then tie your filter to a specific model.

Examples

SQLAlchemy

MongoEngine

Operators

By default, fastapi_filter supports the following operators:

  • neq
  • gt
  • gte
  • in
  • isnull
  • lt
  • lte
  • not/ne
  • not_in/nin
  • like/ilike

Note: Mysql excludes None values when using in filter

For the list related operators (in, not_in), simply pass a comma separated list of strings to your api endpoint and they will be converted into the list of the type you defined.

For example, if the filter field is age__in, you can send ?age__in=1,3,5,7,9.

As long as you setup your query to join your related models, it's pretty straightforward to add related field filters and even re-use existing filters for their models.

As you can see in the examples, all it takes is something along the lines of:

from typing import Optional

class AddressFilter(Filter):
    street: Optional[str]
    country: Optional[str]
    city__in: Optional[list[str]]

    class Constants(Filter.Constants):
        model = Address


class UserFilter(Filter):
    name: Optional[str]
    address: Optional[AddressFilter] = FilterDepends(with_prefix("address", AddressFilter))

    class Constants(Filter.Constants):
        model = User

@app.get("/users", response_model=list[UserOut])
async def get_users(user_filter: UserFilter = FilterDepends(UserFilter), db: AsyncSession = Depends(get_db)) -> Any:
    query = user_filter.filter(select(User).outerjoin(Address))   # (1)
    result = await db.execute(query)
    return result.scalars().all()
  1. See how we need to join with the Address model to be able to filter on it?

The with_prefix wrapper function sets the prefix for your filters, so in that example you would use ?address__city_in=Nantes,Boston for example.

FilterDepends

link

Wherever you would use a Depends, replace it with FilterDepends if you are passing a filter class. The reason is that FilterDepends converts the list filter fields to str so that they can be displayed and used in swagger. It also handles turning ValidationError into HTTPException(status_code=422).

Limitations

FilterDepends does not convert list type to str if it is part of union type with other types except for None. For example type list[str] | None will work but list[str] | str will not.

with_prefix

link

This is a utility function that will take an existing filter and prefix all its fields with a given value. It's mostly used for filtering related fields.

The following would be equivalent:

from typing import Optional

class AddressFilter(Filter):
    street: Optional[str]
    country: Optional[str]

    class Constants(Filter.Constants):
        model = Address


class UserFilter(Filter):
    name: Optional[str]
    address: Optional[AddressFilter] = FilterDepends(with_prefix("address", AddressFilter))

    class Constants(Filter.Constants):
        model = User

AND

class UserFilter(Filter):
    name: Optional[str]
    address__street: Optional[str]
    address__country: Optional[str]

Order by

There is a specific field on the filter class that can be used for ordering. The default name is order_by and it takes a list of string. From an API call perspective, just like the __in filters, you simply pass a comma separated list of strings.

You can change the direction of the sorting (asc or desc) by prefixing with - or + (Optional, it's the default behavior if omitted).

If you don't want to allow ordering on your filter, just don't add order_by (or custom ordering_field_name) as a field and you are all set.

There is a specific field on the filter class that can be used for searching. The default name is search and it takes a string.

You have to define what fields/columns to search in with the search_model_fields constant.

If you don't want to allow searching on your filter, just don't add search (or custom search_field_name) as a field and you are all set.

Example - Basic

from typing import Optional
from fastapi_filter.contrib.sqlalchemy import Filter

class UserFilter(Filter):
    order_by: Optional[list[str]]

@app.get("/users", response_model=list[UserOut])
async def get_users(
    user_filter: UserFilter = FilterDepends(UserFilter),
    db: AsyncSession = Depends(get_db),
) -> Any:
    query = select(User)
    query = user_filter.sort(query)
    result = await db.execute(query)
    return result.scalars().all()

Valid urls:

/users?order_by=age,-created_at
/users
/users?order_by=-name
/users?order_by=+id

Example - Custom name

If for some reason you can't or don't want to use order_by as the field name for ordering, you can override it:

from typing import Optional
from fastapi_filter.contrib.sqlalchemy import Filter

class UserFilter(Filter):
    class Constants(Filter.Constants):
        model = User
        ordering_field_name = "custom_order_by"

    custom_order_by: Optional[list[str]]

@app.get("/users", response_model=list[UserOut])
async def get_users(
    user_filter: UserFilter = FilterDepends(UserFilter),
    db: AsyncSession = Depends(get_db),
) -> Any:
    query = select(User)
    query = user_filter.sort(query)
    result = await db.execute(query)
    return result.scalars().all()

Valid urls:

curl /users?custom_order_by=age,-created_at
curl /users
curl /users?custom_order_by=-name
curl /users?custom_order_by=+id

Restrict the order_by values

Add the following field_validator to your filter class:

from typing import Optional
from fastapi_filter.contrib.sqlalchemy import Filter
from pydantic import field_validator

class MyFilter(Filter):
    order_by: Optional[list[str]]

    @field_validator("order_by")
    def restrict_sortable_fields(cls, value):
        if value is None:
            return None

        allowed_field_names = ["age", "id"]

        for field_name in value:
            field_name = field_name.replace("+", "").replace("-", "")  # (1)
            if field_name not in allowed_field_names:
                raise ValueError(f"You may only sort by: {', '.join(allowed_field_names)}")

        return value
  1. If you want to restrict only on specific directions, like -created_at and name for example, you can remove this line. Your allowed_field_names would be something like ["age", "-age", "-created_at"].

If for some reason you can't or don't want to use search as the field name for searching, you can override it by setting search_field_name:

from typing import Optional
from fastapi_filter.contrib.sqlalchemy import Filter

class UserFilter(Filter):
    class Constants(Filter.Constants):
        model = User
        search_field_name = "custom_name_for_search"
        search_model_fields = ["name", "email"]  # It will search in both `name` and `email` columns.


@app.get("/users", response_model=list[UserOut])
async def get_users(
    user_filter: UserFilter = FilterDepends(UserFilter),
    db: AsyncSession = Depends(get_db),
) -> Any:
    query = select(User)
    query = user_filter.sort(query)
    result = await db.execute(query)
    return result.scalars().all()

Valid urls:

curl /users?custom_name_for_search=Johnny