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, Field
from typing import Optional
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: BookFilterSchema = Query(...)):
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: BookFilterSchema = Query(...)):
# 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 aQ
-expression based on theField
definition of each field; - All
Q
-expressions will be merged into one usingAND
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
name
field will be converted into Q(name=...)
expression.
When your database lookups are more complicated than that, you can explicitly specify them in the field definition using a "q"
kwarg:
class BookFilterSchema(FilterSchema):
name: Optional[str] = Field(None, q='name__icontains')
class BookFilterSchema(FilterSchema):
search: Optional[str] = Field(None, q=['name__icontains',
'author__name__icontains',
'publisher__name__icontains'])
IContainsField = Annotated[Optional[str], Field(None, q='__icontains')]
class BookFilterSchema(FilterSchema):
name: IContainsField
"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.
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: Optional[str] = Field(None, q=['name__icontains', 'author__name__icontains'])
popular: Optional[bool] = None
http://localhost:8000/api/books?search=harry&popular=true
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:
class BookFilterSchema(FilterSchema):
active: Optional[bool] = Field(None, q=['is_active', 'publisher__is_active'],
expression_connector='AND')
name: Optional[str] = Field(None, q='name__icontains')
class Config:
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
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: Optional[str] = Field(None, q='name__icontains')
tag: Optional[str] = Field(None, q='tag', ignore_none=False)
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 settings for all fields at the same time in the Config:
class BookFilterSchema(FilterSchema):
name: Optional[str] = Field(None, q='name__icontains')
tag: Optional[str] = Field(None, q='tag', ignore_none=False)
class Config:
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()
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
custom_expression
method takes precedence over any other definitions described earlier, including filter_<fieldname>
methods.