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:
Since a field name is not specified, this filterset assumes
usernamefield will be available during QuerySet filtering.Since no explicit parameter name is given, the querying will be done using
usernamevia API endpoint.Since
required=Trueis set, this filter will be required and aValidationErrorwill 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:
The root filter allows filtering by company ID using the company query parameter, e.g.,
company=1.The child filter enables filtering by company name using the
company.namequery parameter, e.g.,company.name=google. This is made possible by thelookupargument, which maps both the model field and the query parameter name.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:
{
"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",
]
)
]