Filtering¶
Filtering lets clients request subsets of a list based on field values, related
objects and logical composition (and / or / not). It is built on
Django's own ORM lookups and Q objects — no django-filter dependency.
Overview¶
- Opt-in per type via
Meta.filter_fields. - A single nested
filter:argument of a generated<Model>FilterInputtype. - Per-field lookups (
exact,icontains,in,range,isnull, …). - Relation descent (
author: { name: { … } }), to-many auto-distinct(). - Logical operators:
and,or,not(arbitrarily nested). choicesfields filter through their generated Enum.
Different from graphene-django-extras
The old flat arguments (username: "x", username_Icontains: "x"),
Meta.filterset_class and GraphqlIDFilter are gone. Filtering now
goes through the single nested filter: argument. See the
migration guide.
Declaring filterable fields¶
Meta.filter_fields accepts the same two forms as before:
The default lookup set (used by the list form) is configurable with the
COMMON_FILTER_LOOKUPS setting and is type-aware:
| Field kind | Default lookups |
|---|---|
| any | exact, in, isnull |
| text | + icontains, istartswith |
| number / date / datetime | + gt, gte, lt, lte, range |
Querying with filter:¶
Each declared field becomes a nested object of its lookups:
query {
users(filter: {
username: { icontains: "john" }
isActive: { exact: true }
dateJoined: { gte: "2023-01-01" }
}) {
results { id username email }
totalCount
}
}
Multiple keys in the same object are AND-ed together.
Lookup types¶
| Lookup | Input shape | Meaning |
|---|---|---|
exact |
field: { exact: v } |
equals |
icontains / istartswith |
{ icontains: "ab" } |
case-insensitive contains / starts-with |
gt / gte / lt / lte |
{ gte: 10 } |
ordered comparisons |
in |
{ in: [1, 2, 3] } |
membership (a list) |
range |
{ range: [10, 20] } |
between (a two-element list) |
isnull |
{ isnull: true } |
IS (NOT) NULL |
Only the lookups you declared in filter_fields are exposed on each field.
Logical operators: and / or / not¶
Every <Model>FilterInput carries and: [..], or: [..] and not: {..},
referencing itself — so they nest arbitrarily:
query {
articles(filter: {
status: { exact: PUBLISHED }
or: [
{ views: { lt: 20 } }
{ views: { gte: 100 } }
]
not: { title: { icontains: "draft" } }
}) {
results { title views }
}
}
and: [a, b]→a AND bor: [a, b]→a OR bnot: a→NOT a- sibling keys in the same node are AND-ed with the operators.
Filtering across relations¶
Declare a __ path in filter_fields; it becomes a nested filter input for
the related model, which recurses (and supports its own and/or/not):
A filter that traverses a to-many relation (reverse FK / M2M) automatically
applies .distinct() so join fan-out doesn't duplicate rows.
Filtering by id / pk (incl. UUIDField)¶
Declare the id field — or a relation field directly (not a __ path) — with
scalar lookups, and it filters on the primary key. This replaces the old
GraphqlIDFilter and works for integer and UUID pks:
choices fields filter via their Enum¶
A model field with choices is exposed in the filter input through the same
GraphQL Enum as the output type:
Custom filtering logic¶
There are no custom FilterSet classes anymore. For bespoke rules, override the
queryset hooks on a DjangoModelType (they also scope the generated query/list):
from django.db.models import Q
from django_graphex import DjangoModelType
class UserType(DjangoModelType):
class Meta:
model = User
filter_fields = {"username": ("icontains",)}
@classmethod
def filter_queryset(cls, qs, info, **kwargs):
# e.g. a free-text "search" across several columns
term = info.context.GET.get("q") if hasattr(info.context, "GET") else None
if term:
qs = qs.filter(Q(first_name__icontains=term) | Q(last_name__icontains=term))
return qs
See Permissions & hooks for get_queryset / filter_queryset.
Combining with pagination & ordering¶
Filtering composes with the list field's pagination/ordering, which live on the
results(...) subfield:
{
users(filter: { isActive: { exact: true }, username: { icontains: "jo" } }) {
results(limit: 10, offset: 20, ordering: "-date_joined") {
username email dateJoined
}
totalCount
}
}
Field-level filtering¶
DjangoFilterListField / DjangoFilterPaginateListField expose the same filter:
argument; declare the filterable fields on the underlying type (or pass fields=):
import graphene
from django_graphex import DjangoFilterListField, DjangoFilterPaginateListField
from django_graphex.paginations import PageGraphqlPagination
class Query(graphene.ObjectType):
users = DjangoFilterListField(UserType)
paged_users = DjangoFilterPaginateListField(
UserType, pagination=PageGraphqlPagination(page_size=20)
)
Best practices¶
Tip
- Index frequently-filtered columns (
db_index=True). - Only declare fields you want to expose —
filter_fieldsis the allow-list. - Combine with
get_queryset(select_related/prefetch_related) to keep relation filters efficient. - Use
get_queryset/filter_querysetfor free-text search and any server-forced scoping.