Getting Started

In the following mini-tutorial, we will learn how to make use of some rest-filters features.

To begin, we need a model to construct QuerySet objects from. Since the User model is a common component in most applications, it will serve as our example throughout this guide. Assume the following User model:

class Company(models.Model):
    name = models.CharField(max_length=100)
    address = models.CharField(max_length=100)

    created = models.DateTimeField(auto_now_add=True)


class User(models.Model):
    username = models.CharField(max_length=50)

    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)

    company = models.ForeignKey(
        Company,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="users",
    )
    following_companies = models.ManyToManyField(Company)

    created = models.DateTimeField(auto_now_add=True)

Setting filter backend

Next, we are going to define our view. In this example, we are using a ViewSet to list users:

from rest_filters import FilterBackend


class UserViewSet(ReadOnlyModelViewSet[User]):
    queryset = User.objects.all()
    serializer_class = UserSerializer

    filter_backends = [FilterBackend]

Notice that we need to include our filter backend in filter_backends. As usual, you can set this value globally in REST framework using DEFAULT_FILTER_BACKENDS setting, for example:

REST_FRAMEWORK = {
    "DEFAULT_FILTER_BACKENDS": ["rest_filters.FilterBackend"],
}

Creating your FilterSet

Now we need to define our FilterSet class. To begin, we will create a simple filterset that allows searching users by username:

from rest_filters import Filter, FilterSet
from rest_framework import serializers


class UserFilterSet(FilterSet[User]):
    username = Filter(serializers.CharField(min_length=2), required=True)

This filterset declaration implies a few things:

  1. Since a field name is not specified, this filterset assumes username field will be available during QuerySet filtering.

  2. Since no explicit parameter name is given, the querying will be done using username via API endpoint.

  3. Since required=True is set, this filter will be required and a ValidationError will be raised if users do not specify the query parameter. By default, query parameters are not required.

Note

FilterSet[User] uses the model class as a type variable solely for typing purposes. It does not automatically generate filters or fields based on the model definition.

Using FilterSet in views

Let’s plug this FilterSet into our view. There are two ways to do this. The first method is using filterset_class attribute, just like django-filter:

class UserViewSet(ReadOnlyModelViewSet[User]):
    queryset = User.objects.all()
    serializer_class = UserSerializer

    filter_backends = [FilterBackend]
    filterset_class = UserFilterSet

This method is not suitable if you are using both django-filter and rest-filters at the same time. Since they will both resolve to the same FilterSet class, one of them won’t work.

The second method allows using both libraries together, this involves creating a method called get_filterset_class like so:

class UserViewSet(ReadOnlyModelViewSet[User]):
    queryset = User.objects.all()
    serializer_class = UserSerializer

    filter_backends = [FilterBackend]

    def get_filterset_class(self) -> type[FilterSet]:
        return UserFilterSet

This method is more preferable since it also allows dynamic dispatch of FilterSets based on view actions, permissions, etc.

Navigating to our endpoint, we should be able to filter users by username. We should also get some error messages if something goes wrong, for example:

GET /api/users/
{
    "username": [
        "This field is required."
    ]
}
GET /api/users/?username=a
{
    "username": [
        "Ensure this field has at least 2 characters."
    ]
}
GET /api/users/?usrname=hello
{
    "username": [
        "This field is required."
    ],
    "usrname": [
        "This query parameter does not exist. Did you mean \"username\"?"
    ]
}

Using child filters

Now, let’s implement a more advanced filter. Specifically, we want to search users based on their companies, by both company ID and company name.

class UserFilterSet(FilterSet[User]):
    username = Filter(serializers.CharField(min_length=2))
    company = Filter(
        serializers.IntegerField(min_value=1),
        children=[
            Filter(
                serializers.CharField(min_length=2),
                lookup="name",
            ),
        ],
    )

Let’s digest the company filter:

  1. The root filter allows filtering by company ID using the company query parameter, e.g., company=1.

  2. The child filter enables filtering by company name using the company.name query parameter, e.g., company.name=google. This is made possible by the lookup argument, which maps both the model field and the query parameter name.

  3. Both parent and child filters use different serializer fields, since they require different types. However, fields for child filters might be omitted, in which case they will be inherited from the parent filter.

While this example is useful, the company filter may be unclear to users, as it doesn’t explicitly indicate what attribute is being filtered. To improve this, we can use namespace filters:

company = Filter(
    namespace=True,
    children=[
        Filter(
            serializers.IntegerField(min_value=1),
            lookup="id",
        ),
        Filter(
            serializers.CharField(min_length=2),
            lookup="name",
        ),
    ],
)

This filter exposes two parameters: company.id and company.name.

Using constraints

Depending on your API design, it might not be desirable to make these filters available at the same time. We might force users to only provide id or name using a built-in constraint:

from rest_filters.constraints import MutuallyExclusive


class UserFilterSet(FilterSet[User]):
    username = Filter(serializers.CharField(min_length=2))
    company = Filter(
        namespace=True,
        children=[
            Filter(
                serializers.IntegerField(min_value=1),
                lookup="id",
            ),
            Filter(
                serializers.CharField(min_length=2),
                lookup="name",
            ),
        ],
    )

    class Meta:
        constraints = [
            MutuallyExclusive(
                fields=[
                    "company.id",
                    "company.name",
                ]
            )
        ]

Notice that we used resolved query parameter names while supplying fields for our constraint. This constraint will raise a ValidationError when both fields are used at the same time:

GET /api/users/?company.id=1&company.name=google
 {
     "non_field_errors": [
         "The following fields are mutually exclusive, you may only provide one of them: \"company.id\", \"company.name\""
     ]
 }

Using Filter groups

Up to now, all the filters we used chained filter() calls on QuerySets, since we did not specify any groups. Let’s see an example where using a group would be useful:

following_companies = Filter(
    namespace=True,
    children=[
        Filter(
            serializers.CharField(),
            lookup="name",
        ),
        Filter(
            serializers.CharField(),
            lookup="address",
        ),
    ],
)

This filter allows filtering on users, based on the information of companies they follow. Since we did not specify any group, specifying both of these query parameters will result in a query like this:

SELECT *
  FROM "auth_user"
 INNER JOIN "auth_user_following_companies"
    ON ("auth_user"."id" = "auth_user_following_companies"."user_id")
 INNER JOIN "auth_company"
    ON ("auth_user_following_companies"."company_id" = "auth_company"."id")
 INNER JOIN "auth_user_following_companies" T4
    ON ("auth_user"."id" = T4."user_id")
 INNER JOIN "auth_company" T5
    ON (T4."company_id" = T5."id")
 WHERE ("auth_company"."name" = 'google' AND T5."address" = 'california')

Depending on your use case, this might not be desirable. To limit the joined tables we can group these filters together, by providing group argument on parent filter, from which both of them will inherit. We can also specify groups per filter basis.

Doing this results in a query like this:

SELECT *
  FROM "auth_user"
 INNER JOIN "auth_user_following_companies"
    ON ("auth_user"."id" = "auth_user_following_companies"."user_id")
 INNER JOIN "auth_company"
    ON ("auth_user_following_companies"."company_id" = "auth_company"."id")
 WHERE ("auth_company"."name" = 'google' AND "auth_company"."address" = 'california')

Final FilterSet definition

Here is the final FilterSet with some minor additions for reference:

class UserFilterSet(FilterSet[User]):
    username = Filter(serializers.CharField(min_length=2))
    company = Filter(
        namespace=True,
        children=[
            Filter(
                serializers.IntegerField(min_value=1),
                lookup="id",
            ),
            Filter(
                serializers.CharField(min_length=2),
                lookup="name",
            ),
            Filter(
                serializers.DateTimeField(),
                param="created",
                field="company__created",
                namespace=True,
                children=[
                    Filter(lookup="gte"),
                    Filter(lookup="lte"),
                    Filter(
                        serializers.IntegerField(
                            min_value=1900,
                            max_value=2050,
                        ),
                        lookup="year",
                    ),
                ],
            ),
        ],
    )
    following_companies = Filter(
        namespace=True,
        group="following_companies_group",
        children=[
            Filter(
                serializers.CharField(),
                lookup="name",
            ),
            Filter(
                serializers.CharField(),
                param="address",
                lookup="address__icontains",
            ),
        ],
    )
    created = Filter(
        serializers.DateTimeField(),
        namespace=True,
        children=[
            Filter(lookup="gte"),
            Filter(lookup="lte"),
        ],
    )

    class Meta:
        constraints = [
            MutuallyExclusive(
                fields=[
                    "company.id",
                    "company.name",
                ]
            )
        ]