Skip to content

# Filtering

If you want to allow the user to filter your querysets by a number of different attributes, it makes sense to encapsulate your filters into a `FilterSchema` class. `FilterSchema` is a regular `Schema`, so it's using all the necessary features of Pydantic, but it also adds some bells and whistles that ease the translation of the user-facing filtering parameters into database queries.

Start off with defining a subclass of `FilterSchema`:

    from ninja import FilterSchema
    from typing import Optional
    from datetime import datetime

    class BookFilterSchema(FilterSchema):
        name: Optional[str] = None
        author: Optional[str] = None
        created_after: Optional[datetime] = None

Next, use this schema in conjunction with `Query` in your API handler:

    @api.get("/books")
    def list_books(request, filters: BookFilterSchema = Query(...)):
        books = Book.objects.all()
        books = filters.filter(books)
        return books

Just like described in defining query params using schema, Django Ninja converts the fields defined in `BookFilterSchema` into query parameters.

You can use a shorthand one-liner `.filter()` to apply those filters to your queryset:

    @api.get("/books")
    def list_books(request, filters: Query[BookFilterSchema]):
        books = Book.objects.all()
        books = filters.filter(books)
        return books

Under the hood, `FilterSchema` converts its fields into Q expressions which it then combines and uses to filter your queryset.

Alternatively to using the `.filter` method, you can get the prepared `Q`-expression and perform the filtering yourself. That can be useful, when you have some additional queryset filtering on top of what you expose to the user through the API:

    @api.get("/books")
    def list_books(request, filters: Query[BookFilterSchema]):

        # Never serve books from inactive publishers and authors
        q = Q(author__is_active=True) | Q(publisher__is_active=True)

        # But allow filtering the rest of the books
        q &= filters.get_filter_expression()
        return Book.objects.filter(q)

By default, the filters will behave the following way:

- `None` values will be ignored and not filtered against;
- Every non-`None` field will be converted into a `Q`-expression based on the `Field` definition of each field;
- All `Q`-expressions will be merged into one using `AND` logical operator;
- The resulting `Q`-expression is used to filter the queryset and return you a queryset with a `.filter` clause applied.

## Customizing Fields

By default, `FilterSet` will use the field names to generate Q expressions:

    class BookFilterSchema(FilterSchema):
        name: Optional[str] = None

The `name` field will be converted into `Q(name=...)` expression.

When your database lookups are more complicated than that, you can annotate your fields with an instance of `FilterLookup` where you specify how you wish your field to be looked up for filtering:

    from ninja import FilterSchema, FilterLookup
    from typing import Annotated

    class BookFilterSchema(FilterSchema):
        name: Annotated[Optional[str], FilterLookup("name__icontains")] = None

You can even specify multiple lookups as a list:

    class BookFilterSchema(FilterSchema):
        search: Annotated[Optional[str], FilterLookup(
            ["name__icontains",
             "author__name__icontains",
             "publisher__name__icontains"]
        )]

By default, field-level expressions are combined using `"OR"` connector, so with the above setup, a query parameter `?search=foobar` will search for books that have "foobar" in either of their name, author or publisher.

And to make generic fields, you can make the field name implicit by skipping it:

    IContainsField = Annotated[Optional[str], FilterLookup('__icontains')]

    class BookFilterSchema(FilterSchema):
        name: IContainsField = None

Deprecated syntax

In previous versions, database lookups were specified using `Field(q=...)` syntax:

    from ninja import FilterSchema, Field

    class BookFilterSchema(FilterSchema):
        name: Optional[str] = Field(None, q="name__icontains")

This approach is still supported, but it is considered deprecated and not recommended for new code because:

- Poor IDE support (IDEs don't recognize custom `Field` arguments)
- Uses deprecated Pydantic features (`**extra`)
- Less type-safe and harder to maintain

The new `FilterLookup` annotation provides better developer experience with full IDE support and type safety. Prefer using `FilterLookup` for new projects.

## Combining expressions

By default,

- Field-level expressions are joined together using `OR` operator.
- The fields themselves are joined together using `AND` operator.

So, with the following `FilterSchema`...

    class BookFilterSchema(FilterSchema):
        search: Annotated[
            Optional[str],
            FilterLookup(["name__icontains", "author__name__icontains"])] = None
        popular: Optional[bool] = None

...and the following query parameters from the user

    http://localhost:8000/api/books?search=harry&popular=true

the `FilterSchema` instance will look for popular books that have `harry` in the book's or author's name.

You can customize this behavior using an `expression_connector` argument in field-level and class-level definition:

    from ninja import FilterConfigDict, FilterLookup, FilterSchema

    class BookFilterSchema(FilterSchema):
        active: Annotated[
            Optional[bool],
            FilterLookup(
                ["is_active", "publisher__is_active"],
                expression_connector="AND"
            )] = None
        name: Annotated[Optional[str], FilterLookup("name__icontains")] = None

        model_config = FilterConfigDict(expression_connector="OR")

An expression connector can take the values of `"OR"`, `"AND"` and `"XOR"`, but the latter is only supported in Django starting with 4.1.

Now, a request with these query parameters

    http://localhost:8000/api/books?name=harry&active=true

...shall search for books that have `harry` in their name or are active themselves and are published by active publishers.

## Filtering by Nones

You can make the `FilterSchema` treat `None` as a valid value that should be filtered against.

This can be done on a field level with a `ignore_none` kwarg:

    class BookFilterSchema(FilterSchema):
        name: Annotated[Optional[str], FilterLookup("name__icontains")] = None
        tag: Annotated[Optional[str], FilterLookup("tag", ignore_none=False)] = None

This way when no other value for `"tag"` is provided by the user, the filtering will always include a condition `tag=None`.

You can also specify this setting for all fields at the same time in `model_config`:

    class BookFilterSchema(FilterSchema):
        name: Annotated[Optional[str], FilterLookup("name__icontains")] = None
        tag: Optional[str] = None

        model_config = FilterConfigDict(ignore_none=False)

## Custom expressions

Sometimes you might want to have complex filtering scenarios that cannot be handled by individual Field annotations. For such cases you can implement your field filtering logic as a custom method. Simply define a method called `filter_<fieldname>` which takes a filter value and returns a Q expression:

    class BookFilterSchema(FilterSchema):
        tag: Optional[str] = None
        popular: Optional[bool] = None

        def filter_popular(self, value: bool) -> Q:
            return Q(view_count__gt=1000) | Q(download_count__gt=100) if value else Q()

Such field methods take precedence over what is specified in the `Field()` definition of the corresponding fields.

If that is not enough, you can implement your own custom filtering logic for the entire `FilterSet` class in a `custom_expression` method:

    class BookFilterSchema(FilterSchema):
        name: Optional[str] = None
        popular: Optional[bool] = None

        def custom_expression(self) -> Q:
            q = Q()
            if self.name:
                q &= Q(name__icontains=self.name)
            if self.popular:
                q &= (
                    Q(view_count__gt=1000) |
                    Q(downloads__gt=100) |
                    Q(tag='popular')
                )
            return q

The `custom_expression` method takes precedence over any other definitions described earlier, including `filter_<fieldname>` methods.