Skip to content

Filtering

It is possible to define filters for Django types, which will be converted into .filter(...) queries for the ORM:

types.py
import strawberry_django
from strawberry import auto

@strawberry_django.filter(models.Fruit)
class FruitFilter:
    id: auto
    name: auto

@strawberry_django.type(models.Fruit, filters=FruitFilter)
class Fruit:
    ...

Tip

In most cases filter fields should have Optional annotations and default value strawberry.UNSET like so: foo: Optional[SomeType] = strawberry.UNSET Above auto annotation is wrapped in Optional automatically. UNSET is automatically used for fields without field or with strawberry_django.filter_field.

The code above would generate following schema:

schema.graphql
1
2
3
4
5
6
7
8
input FruitFilter {
  id: ID
  name: String
  AND: FruitFilter
  OR: FruitFilter
  NOT: FruitFilter
  DISTINCT: Boolean
}

Tip

If you are using the relay integration and working with types inheriting from relay.Node and GlobalID for identifying objects, you might want to set MAP_AUTO_ID_AS_GLOBAL_ID=True in your strawberry django settings to make sure auto fields gets mapped to GlobalID on types and filters.

AND, OR, NOT, DISTINCT ...

To every filter AND, OR, NOT & DISTINCT fields are added to allow more complex filtering

{
  fruits(
    filters: {
      name: "kebab"
      OR: {
        name: "raspberry"
      }
    }
  ) { ... }
}

Lookups

Lookups can be added to all fields with lookups=True, which will add more options to resolve each type. For example:

types.py
1
2
3
4
@strawberry_django.filter(models.Fruit, lookups=True)
class FruitFilter:
    id: auto
    name: auto

The code above would generate the following schema:

schema.graphql
input IDBaseFilterLookup {
  exact: ID
  isNull: Boolean
  inList: [String!]
}

input StrFilterLookup {
  exact: ID
  isNull: Boolean
  inList: [String!]
  iExact: String
  contains: String
  iContains: String
  startsWith: String
  iStartsWith: String
  endsWith: String
  iEndsWith: String
  regex: String
  iRegex: String
}

input FruitFilter {
  id: IDFilterLookup
  name: StrFilterLookup
  AND: FruitFilter
  OR: FruitFilter
  NOT: FruitFilter
  DISTINCT: Boolean
}

Single-field lookup can be annotated with the FilterLookup generic type.

types.py
1
2
3
4
5
from strawberry_django import FilterLookup

@strawberry_django.filter(models.Fruit)
class FruitFilter:
    name: FilterLookup[str]

Filtering over relationships

types.py
@strawberry_django.filter(models.Color)
class ColorFilter:
    id: auto
    name: auto

@strawberry_django.filter(models.Fruit)
class FruitFilter:
    id: auto
    name: auto
    color: ColorFilter | None

The code above would generate following schema:

schema.graphql
input ColorFilter {
  id: ID
  name: String
  AND: ColorFilter
  OR: ColorFilter
  NOT: ColorFilter
}

input FruitFilter {
  id: ID
  name: String
  color: ColorFilter
  AND: FruitFilter
  OR: FruitFilter
  NOT: FruitFilter
}

Custom filter methods

You can define custom filter method by defining your own resolver.

types.py
@strawberry_django.filter(models.User)
class FruitFilter:
    name: auto
    last_name: auto

    @strawberry_django.filter_field
    def simple(self, value: str, prefix) -> Q:
        return Q(**{f"{prefix}name": value})

    @strawberry_django.filter_field
    def full_name(
        self,
        queryset: QuerySet,
        value: str,
        prefix: str
    ) -> tuple[QuerySet, Q]:
        queryset = queryset.alias(
            _fullname=Concat(
                f"{prefix}name", Value(" "), f"{prefix}last_name"
            )
        )
        return queryset, Q(**{"_fullname": value})

    @strawberry_django.filter_field
    def full_name_lookups(
        self,
        info: Info,
        queryset: QuerySet,
        value: strawberry_django.FilterLookup[str],
        prefix: str
    ) -> tuple[QuerySet, Q]:
        queryset = queryset.alias(
            _fullname=Concat(
                f"{prefix}name", Value(" "), f"{prefix}last_name"
            )
        )
        return strawberry_django.process_filters(
            filters=value,
            queryset=queryset,
            info=info,
            prefix=f"{prefix}_fullname"
        )

Warning

It is discouraged to use queryset.filter() directly. When using more complex filtering via NOT, OR & AND this might lead to undesired behaviour.

Tip

process_filters

As seen above strawberry_django.process_filters function is exposed and can be reused in custom methods. Above it's used to resolve fields lookups

null values

By default null value is ignored for all filters & lookups. This applies to custom filter methods as well. Those won't even be called (you don't have to check for None). This can be modified using strawberry_django.filter_field(filter_none=True)

This also means that build in exact & iExact lookups cannot be used to filter for None and isNull have to be used explicitly.

value resolution

  • value parameter of type relay.GlobalID is resolved to its node_id attribute
  • value parameter of type Enum is resolved to is's value
  • above types are converted in lists as well

resolution can modified via strawberry_django.filter_field(resolve_value=...)

  • True - always resolve
  • False - never resolve
  • UNSET (default) - resolves for filters without custom method only

The code above generates the following schema:

schema.graphql
1
2
3
4
5
6
7
input FruitFilter {
  name: String
  lastName: String
  simple: str
  fullName: str
  fullNameLookups: StrFilterLookup
}

Resolver arguments

  • prefix - represents the current path or position
  • Required
  • Important for nested filtering
  • In code bellow custom filter name ends up filtering Fruit instead of Color without applying prefix
Why prefix?
@strawberry_django.filter(models.Fruit)
class FruitFilter:
    name: auto
    color: ColorFilter | None

@strawberry_django.filter(models.Color)
class ColorFilter:
    @strawberry_django.filter_field
    def name(self, value: str, prefix: str):
        # prefix is "fruit_set__" if unused root object is filtered instead
        if value:
            return Q(name=value)
        return Q()
1
2
3
{
  fruits( filters: {color: name: "blue"} ) { ... }
}
  • value - represents graphql field type
  • Required, but forbidden for default filter method
  • must be annotated
  • used instead of field's return type
  • queryset - can be used for more complex filtering
  • Optional, but Required for default filter method
  • usually used to annotate QuerySet

Resolver return

For custom field methods two return values are supported

  • django's Q object
  • tuple with QuerySet and django's Q object -> tuple[QuerySet, Q]

For default filter method only second variant is supported.

What about nulls?

By default null values are ignored. This can be toggled as such @strawberry_django.filter_field(filter_none=True)

Overriding the default filter method

Works similar to field filter method, but:

  • is responsible for resolution of filtering for entire object
  • must be named filter
  • argument queryset is Required
  • argument value is Forbidden
types.py
@strawberry_django.filter(models.Fruit)
class FruitFilter:
    def ordered(
        self,
        value: int,
        prefix: str,
        queryset: QuerySet,
    ):
        queryset = queryset.alias(
          _ordered_num=Count(f"{prefix}orders__id")
        )
        return queryset, Q(**{f"{prefix}_ordered_num": value})

    @strawberry_django.order_field
    def filter(
        self,
        info: Info,
        queryset: QuerySet,
        prefix: str,
    ) -> tuple[QuerySet, list[Q]]:
        queryset = queryset.filter(
            ... # Do some query modification
        )

        return strawberry_django.process_filters(
            self,
            info=info,
            queryset=queryset,
            prefix=prefix,
            skip_object_order_method=True
        )

Tip

As seen above strawberry_django.process_filters function is exposed and can be reused in custom methods. For filter method filter skip_object_order_method was used to avoid endless recursion.

Adding filters to types

All fields and CUD mutations inherit filters from the underlying type by default. So, if you have a field like this:

types.py
1
2
3
4
5
6
7
@strawberry_django.type(models.Fruit, filters=FruitFilter)
class Fruit:
    ...

@strawberry.type
class Query:
    fruits: list[Fruit] = strawberry_django.field()

The fruits field will inherit the filters of the type in the same way as if it was passed to the field.

Adding filters directly into a field

Filters added into a field override the default filters of this type.

schema.py
1
2
3
@strawberry.type
class Query:
    fruits: list[Fruit] = strawberry_django.field(filters=FruitFilter)

Generic Lookup reference

There is 7 already defined Generic Lookup strawberry.input classes importable from strawberry_django

BaseFilterLookup

  • contains exact, isNull & inList
  • used for ID & bool fields

RangeLookup

  • used for range or BETWEEN filtering

ComparisonFilterLookup

  • inherits BaseFilterLookup
  • additionaly contains gt, gte, lt, lte, & range
  • used for Numberical fields

FilterLookup

  • inherits BaseFilterLookup
  • additionally contains iExact, contains, iContains, startsWith, iStartsWith, endsWith, iEndsWith, regex & iRegex
  • used for string based fields and as default

DateFilterLookup

  • inherits ComparisonFilterLookup
  • additionally contains year,month,day,weekDay,isoWeekDay,week,isoYear & quarter
  • used for date based fields

TimeFilterLookup

  • inherits ComparisonFilterLookup
  • additionally contains hour,minute,second,date & time
  • used for time based fields

DatetimeFilterLookup

  • inherits DateFilterLookup & TimeFilterLookup
  • used for timedate based fields

Legacy filtering

The previous version of filters can be enabled via USE_DEPRECATED_FILTERS

Warning

If USE_DEPRECATED_FILTERS is not set to True legacy custom filtering methods will be not be called.

When using legacy filters it is important to use legacy strawberry_django.filters.FilterLookup lookups as well. The correct version is applied for auto annotated filter field (given lookups=True being set). Mixing old and new lookups might lead to error DuplicatedTypeName: Type StrFilterLookup is defined multiple times in the schema.

While legacy filtering is enabled new filtering custom methods are fully functional including default filter method.

Migration process could be composed of these steps:

  • enable USE_DEPRECATED_FILTERS
  • gradually transform custom filter field methods to new version (do not forget to use old FilterLookup if applicable)
  • gradually transform default filter methods
  • disable USE_DEPRECATED_FILTERS - This is breaking change