Skip to content

Fields

Complete guide to all field types, lookups, type annotations, PostgreSQL features, and every nitpick detail about fields in drf-restflow.

Quick Navigation

Getting Started

Field Types by Category

Basic Fields - StringField - Text filtering with text lookups - IntegerField - Integer filtering with comparison lookups - FloatField - Floating-point number filtering - BooleanField - True/False filtering

Numeric Fields - DecimalField - Precise decimal number filtering

Date & Time Fields - DateField - Date filtering with year/month/day lookups - DateTimeField - DateTime filtering with timestamp lookups - TimeField - Time-only filtering

Choice Fields - ChoiceField - Single choice from predefined options - MultipleChoiceField - Multiple choices from options

Collection Fields - ListField - List/array filtering with __in lookup

Ordering - OrderField - Sorting and ordering results

Relational Fields - RelatedField - Filters across relationship fields in django model

PostgreSQL Fields - Full-Text Search - SearchVector and SearchQuery - Array Fields - PostgreSQL array operations - JSON Fields - JSONField filtering - Range Fields - Date/Integer ranges - Trigram Search - Fuzzy text matching

Advanced Topics

Field Basics

Fields define what query parameters your FilterSet accepts and how they filter the queryset.

Three Ways to Declare Fields

from restflow.filters import FilterSet, StringField

class ProductFilterSet(FilterSet):
    # 1. Type annotation (simplest)
    name: str

    # 2. Explicit field
    description = StringField(lookups=["icontains"])

    # 3. Model-based with extra_kwargs
    class Meta:
        model = Product
        fields = ['price']
        extra_kwargs = {
            'price': {'min_value': 0}
        }

Field Generation

Each field can generate multiple filter parameters:

class ProductFilterSet(FilterSet):
    price = IntegerField(lookups=["comparison"])

# Generates:
# - price           (exact match)
# - price__gt       (greater than)
# - price__gte      (greater than or equal)
# - price__lt       (less than)
# - price__lte      (less than or equal)
# - price!          (not equal - negation)
# - price__gt!      (not greater than)
# - price__gte!     (not greater than or equal)
# - price__lt!      (not less than)
# - price__lte!     (not less than or equal)

Type Annotations

Use Python type annotations for automatic field generation.

Basic Types

from typing import List, Literal
from datetime import datetime, date, time
from decimal import Decimal

class ProductFilterSet(FilterSet):
    # String → StringField
    name: str
    description: str

    # Integer → IntegerField
    quantity: int
    views: int

    # Float → FloatField
    rating: float
    score: float

    # Boolean → BooleanField
    in_stock: bool
    is_featured: bool

    # Decimal → DecimalField
    price: Decimal

    # Date/Time → DateField/DateTimeField/TimeField
    created_date: date
    created_at: datetime
    opening_time: time

Choice Types with Literal

from typing import Literal

class ProductFilterSet(FilterSet):
    # ChoiceField
    status: Literal["draft", "published", "archived"]

    # Also works with numbers
    priority: Literal[1, 2, 3, 4, 5]

List Types

from typing import List

class ProductFilterSet(FilterSet):
    # ListField with IntegerField child
    tags: List[int]

    # ListField with StringField child
    categories: List[str]

    # Can specify lookup expression
    tag_ids: List[int]  # Becomes tags__in by default

Optional Types

from typing import Optional

class ProductFilterSet(FilterSet):
    # Optional fields (not required)
    category: Optional[str]
    brand: Optional[int]

    # Or Modern style
    price : int | None

All Field Types

StringField

For text filtering.

from restflow.filters import StringField

class ProductFilterSet(FilterSet):
    # Basic string field
    name = StringField()

    # With lookups
    title = StringField(lookups=["icontains", "istartswith"])

    # With validation
    sku = StringField(
        min_length=3,
        max_length=20,
        required=True
    )

    # Custom lookup expression
    category_name = StringField(lookup_expr="category__name__icontains")

Available lookups: - exact: Exact match (default) - iexact: Case-insensitive exact match - contains: Contains substring - icontains: Case-insensitive contains - startswith: Starts with - istartswith: Case-insensitive starts with - endswith: Ends with - iendswith: Case-insensitive ends with - regex: Regular expression - iregex: Case-insensitive regex

Lookup category: - text: Expands to icontains, contains, startswith, endswith, iexact

Parameters: - min_length: Minimum string length - max_length: Maximum string length - required: Make field required - validators: List of custom validators - help_text: Description - trim_whitespace: Remove leading/trailing whitespace (default: True) - allow_blank: Allow empty strings - lookups: List of lookup variations - lookup_expr: Custom Django ORM lookup expression - method: Custom filter method

IntegerField

For integer number filtering.

from restflow.filters import IntegerField

class ProductFilterSet(FilterSet):
    # Basic integer field
    quantity: int

    # With lookups
    price = IntegerField(lookups=["comparison"])
    # Creates: price, price__gt, price__gte, price__lt, price__lte

    # With validation
    stock = IntegerField(
        min_value=0,
        max_value=10000,
        required=False
    )

    # Related field
    category_id = IntegerField(lookup_expr="category__id")

Available lookups: - exact: Exact match (default) - gt: Greater than - gte: Greater than or equal - lt: Less than - lte: Less than or equal - in: In list - range: Between two values

Lookup category: - comparison: Expands to gt, gte, lt, lte

Parameters: - min_value: Minimum allowed value - max_value: Maximum allowed value - required: Make field required - validators: List of custom validators - help_text: Description - lookups: List of lookup variations - lookup_expr: Custom lookup expression - method: Custom filter method

FloatField

For floating-point number filtering.

from restflow.filters import FloatField

class ProductFilterSet(FilterSet):
    # Basic float field
    rating: float

    # With lookups
    score = FloatField(lookups=["comparison"])

    # With validation
    discount = FloatField(
        min_value=0.0,
        max_value=100.0
    )

    # Average rating (annotated field)
    min_rating = FloatField(
        method="filter_min_rating",
        min_value=0.0,
        max_value=5.0
    )

Available lookups: - exact: Exact match (default) - gt, gte, lt, lte: Comparison - range: Between two values

Lookup category: - comparison: Expands to gt, gte, lt, lte

Parameters: - min_value: Minimum value - max_value: Maximum value - required: Make field required - validators: List of validators - help_text: Description - lookups: Lookup variations - lookup_expr: Custom lookup - method: Custom method

BooleanField

For boolean filtering.

from restflow.filters import BooleanField

class ProductFilterSet(FilterSet):
    # Basic boolean
    in_stock: bool

    # Explicit declaration
    is_featured = BooleanField()

    # With custom method
    available = BooleanField(method="filter_available")

    # Required boolean
    active = BooleanField(required=True)

Accepts values: - True: true, True, 1, yes - False: false, False, 0, no

Available lookups: - exact: Exact match (default) - isnull: Check if null

Parameters: - required: Make field required - help_text: Description - lookup_expr: Custom lookup - method: Custom method

DecimalField

For precise decimal number filtering (e.g., money).

from restflow.filters import DecimalField

class ProductFilterSet(FilterSet):
    # Basic decimal
    price = DecimalField(
        max_digits=10,
        decimal_places=2
    )

    # With lookups and validation
    amount = DecimalField(
        max_digits=10,
        decimal_places=2,
        min_value=0,
        lookups=["comparison"]
    )

Available lookups: - exact, gt, gte, lt, lte, range

Lookup category: - comparison

Parameters: - max_digits: Total digits (including decimal places) - decimal_places: Number of decimal places - min_value, max_value: Validation - required, validators, help_text, lookups, lookup_expr, method

DateField

For date filtering.

from restflow.filters import DateField

class ProductFilterSet(FilterSet):
    # Basic date
    created_date: date

    # With lookups
    published_date = DateField(lookups=["comparison"])
    # Creates: published_date, published_date__gt, __gte, __lt, __lte

    # Date range
    start_date = DateField(lookup_expr="created_at__date__gte")
    end_date = DateField(lookup_expr="created_at__date__lte")

Accepts formats: - ISO 8601: 2024-01-15 - Other formats configured in DRF settings

Available lookups: - exact, gt, gte, lt, lte - year, month, day - week, week_day - quarter

Lookup category: - comparison

Parameters: - input_formats: List of accepted date formats - required, validators, help_text, lookups, lookup_expr, method

DateTimeField

For datetime filtering.

from restflow.filters import DateTimeField

class ProductFilterSet(FilterSet):
    # Basic datetime
    created_at: datetime

    # With lookups
    published_at = DateTimeField(lookups=["comparison"])

    # With timezone support
    updated_at = DateTimeField()

Accepts formats: - ISO 8601: 2024-01-15T10:30:00Z - Other formats configured in DRF settings

Available lookups: - Same as DateField plus time-specific - hour, minute, second

Lookup category: - comparison

Parameters: - input_formats: Accepted datetime formats - default_timezone: Timezone for naive datetimes - required, validators, help_text, lookups, lookup_expr, method

TimeField

For time filtering.

from restflow.filters import TimeField

class ProductFilterSet(FilterSet):
    # Basic time
    opening_time: time

    # With lookups
    closing_time = TimeField(lookups=["comparison"])

Accepts formats: - 14:30:00 - 14:30

Available lookups: - exact, gt, gte, lt, lte - hour, minute, second

Parameters: - input_formats, required, validators, help_text, lookups, lookup_expr, method

ChoiceField

For fields with limited choices.

from restflow.filters import ChoiceField

class ProductFilterSet(FilterSet):
    # Basic choices
    status = ChoiceField(
        choices=[
            ('draft', 'Draft'),
            ('published', 'Published'),
            ('archived', 'Archived')
        ]
    )

    # With Literal type annotation
    priority: Literal["low", "medium", "high", "urgent"]

    # With tuple choices
    size = ChoiceField(
        choices=[
            ('S', 'Small'),
            ('M', 'Medium'),
            ('L', 'Large'),
            ('XL', 'Extra Large')
        ]
    )

Available lookups: - exact (default) - in

Parameters: - choices: List of (value, label) tuples or list of values - required, help_text, lookups, lookup_expr, method

MultipleChoiceField

For selecting multiple choices.

from restflow.filters import MultipleChoiceField

class ProductFilterSet(FilterSet):
    # Multiple statuses
    statuses = MultipleChoiceField(
        choices=[
            ('draft', 'Draft'),
            ('published', 'Published')
        ],
        lookup_expr="status__in"
    )

    # Multiple categories
    categories = MultipleChoiceField(
        choices=[
            ('electronics', 'Electronics'),
            ('books', 'Books'),
            ('clothing', 'Clothing')
        ]
    )

Accepts: - Comma-separated values: ?statuses=draft,published

Available lookups: - in (default)

Parameters: - choices: List of (value, label) tuples - required, help_text, lookup_expr, method

ListField

For filtering by list of values.

from restflow.filters import ListField, IntegerField, StringField

class ProductFilterSet(FilterSet):
    # List of integers
    tags: List[int]
    # Auto-generates: tags__in

    # Explicit declaration
    tag_ids = ListField(
        child=IntegerField(),
        lookup_expr="tags__id__in"
    )

    # List of strings
    categories = ListField(
        child=StringField(),
        lookup_expr="category__name__in"
    )

    # With validation
    product_ids = ListField(
        child=IntegerField(min_value=1),
        min_length=1,
        max_length=100
    )

Accepts: - Comma-separated: ?tags=1,2,3 - Multiple params: ?tags=1&tags=2&tags=3

Available lookups: - in (default) - overlap (PostgreSQL arrays) - contains (PostgreSQL arrays) - contained_by (PostgreSQL arrays)

Lookup category: - pg_array: PostgreSQL array lookups

Parameters: - child: Field type for list items - min_length: Minimum number of items - max_length: Maximum number of items - required, help_text, lookups, lookup_expr, method

OrderField

For ordering/sorting.

from restflow.filters import OrderField

class ProductFilterSet(FilterSet):
    ordering = OrderField(
        fields=[
            ('name', 'name'),
            ('price', 'price'),
            ('created_at', 'created_at')
        ]
    )

# Usage:
# ?ordering=name           # Ascending
# ?ordering=-price         # Descending

Note: Usually defined in Meta.order_fields instead. See FilterSet guide.

RelatedField

Filter across relationships:

from restflow.filters import RelatedField

class Product(models.Model):
    name = models.CharField(max_length=200)
    category = models.ForeignKey(Category, on_delete=models.CASCADE)

class Category(models.Model):
    name = models.CharField(max_length=100)
    description = models.TextField()
    sku = models.CharField(max_length=12)

# FilterSet with related fields
class ProductFilterSet(FilterSet):
    category = RelatedField(
        model=Category,
        fields=["name", "description"],
        exclude=["sku"]
    )

# Generates filters:
# - category__name
# - category__name__icontains (if lookups configured)
# - category__description
# - category__description__icontains

# Usage
?category__name=Electronics
?category__name__icontains=elec

Lookups

Lookups allow creating variations of a field for different filter operations.

Individual Lookups

class ProductFilterSet(FilterSet):
    # Exact match only (default)
    name: str

    # Exact + contains
    title = StringField(lookups=["exact", "icontains"])
    # Creates: title, title__icontains

    # Comparison lookups
    price = IntegerField(lookups=["gt", "gte", "lt", "lte"])
    # Creates: price__gt, price__gte, price__lt, price__lte

    # Date lookups
    created_at = DateTimeField(lookups=["gte", "lte", "year", "month"])
    # Creates: created_at__gte, created_at__lte, created_at__year, created_at__month

Lookup Categories

Use predefined groups instead of listing individual lookups:

class ProductFilterSet(FilterSet):
    # "text" category
    name = StringField(lookups=["text"])
    # Expands to: icontains, contains, startswith, endswith, iexact

    # "comparison" category
    price = IntegerField(lookups=["comparison"])
    # Expands to: gt, gte, lt, lte

    # "pg_array" category (PostgreSQL)
    tags = ListField(child=StringField(), lookups=["pg_array"])
    # Expands to: contains, overlap, contained_by

Available categories:

  • text (StringField): icontains, contains, startswith, endswith, iexact
  • comparison (Numeric/Date fields): gt, gte, lt, lte
  • pg_array (PostgreSQL arrays): contains, overlap, contained_by
  • pg_json (PostgreSQL JSON): JSON-specific lookups

All Django Lookup Expressions

# Text lookups
exact, iexact
contains, icontains
startswith, istartswith
endswith, iendswith
regex, iregex

# Numeric/Date lookups
gt, gte, lt, lte
range
in

# Null checks
isnull

# Date component lookups
year, month, day
week, week_day, quarter
hour, minute, second

# PostgreSQL-specific
contains (array/range)
contained_by (array/range)
overlap (array/range)
has_key, has_keys, has_any_keys (JSON)

# Geographic (GeoDjango)
distance_lt, distance_lte, distance_gt, distance_gte
dwithin

Negation with Lookups

Every lookup automatically gets a negation variant:

class ProductFilterSet(FilterSet):
    price = IntegerField(lookups=["gte", "lte"])

# Generates:
# price__gte      # Greater than or equal
# price__gte!     # NOT greater than or equal
# price__lte      # Less than or equal
# price__lte!     # NOT less than or equal

Negation

All fields automatically support negation with ! suffix.

Basic Negation

# ?status!=draft              # Status NOT draft
# ?in_stock!=true            # NOT in stock
# ?price!=1000               # Price NOT 1000

Negation with Lookups

# ?price__gte!=100           # NOT (price >= 100)  →  price < 100
# ?name__icontains!=test     # Name does NOT contain "test"
# ?created_at__year!=2024    # NOT created in 2024

Negation Examples

class ProductFilterSet(FilterSet):
    name = StringField(lookups=["icontains"])
    price = IntegerField(lookups=["comparison"])
    status: str

# All work:
# ?name!=laptop                  # Name not "laptop"
# ?name__icontains!=wireless     # Name doesn't contain "wireless"
# ?price!=99                     # Price not 99
# ?price__gte!=1000             # Price NOT >= 1000 (i.e., < 1000)
# ?status!=draft                # Status not draft

Multiple Negations

# Combine multiple negations
?status!=draft&status!=archived&in_stock!=false

# With AND operator: NOT draft AND NOT archived AND in_stock

Custom Lookup Expressions

Override the default lookup expression to filter by related fields or custom paths.

Basic Custom Expressions

class ProductFilterSet(FilterSet):
    # Filter by related field
    category_name = StringField(lookup_expr="category__name")

    # Case-insensitive related field
    brand = StringField(lookup_expr="brand__name__iexact")

    # Multiple relationship levels
    department = StringField(lookup_expr="category__department__name")

# Usage:
# ?category_name=Electronics
# ?brand=apple
# ?department=Technology

With Lookups

class ProductFilterSet(FilterSet):
    # Base expression + lookups
    category_name = StringField(
        lookup_expr="category__name",
        lookups=["icontains", "istartswith"]
    )

# Generates:
# category_name__icontains       → category__name__icontains
# category_name__istartswith     → category__name__istartswith

Nested Relationships

# Models
class Region(models.Model):
    name = models.CharField(max_length=100)

class City(models.Model):
    name = models.CharField(max_length=100)
    region = models.ForeignKey(Region, on_delete=models.CASCADE)

class Address(models.Model):
    street = models.CharField(max_length=200)
    city = models.ForeignKey(City, on_delete=models.CASCADE)

class Store(models.Model):
    name = models.CharField(max_length=100)
    address = models.ForeignKey(Address, on_delete=models.CASCADE)

class Product(models.Model):
    name = models.CharField(max_length=200)
    store = models.ForeignKey(Store, on_delete=models.CASCADE)

# FilterSet
class ProductFilterSet(FilterSet):
    # Traverse multiple relationships
    region = StringField(lookup_expr="store__address__city__region__name")
    city = StringField(lookup_expr="store__address__city__name")

# Or Can be done using related field

# Usage:
# ?region=California
# ?city=San Francisco

Date Component Lookups

class ProductFilterSet(FilterSet):
    # Filter by date components
    created_year = IntegerField(lookup_expr="created_at__year")
    created_month = IntegerField(lookup_expr="created_at__month")
    created_day = IntegerField(lookup_expr="created_at__day")

    # Or with lookups
    published_year = IntegerField(
        lookup_expr="published_at__year",
        lookups=["gte", "lte"]
    )

# Usage:
# ?created_year=2024
# ?created_month=6
# ?published_year__gte=2020

Annotated Field Lookups

from django.db.models import Count

def add_annotations(filterset, queryset):
    return queryset.annotate(
        review_count=Count('reviews'),
        avg_rating=Avg('reviews__rating')
    )

class ProductFilterSet(FilterSet):
    # Filter by annotated fields
    min_reviews = IntegerField(lookup_expr="review_count__gte")
    max_reviews = IntegerField(lookup_expr="review_count__lte")

    class Meta:
        preprocessors = [add_annotations]

# Usage:
# ?min_reviews=10
# ?max_reviews=100

Validation

Automatic Type Validation

class ProductFilterSet(FilterSet):
    price: int        # Only accepts integers
    rating: float     # Only accepts floats
    in_stock: bool    # Only accepts true/false

# Invalid input returns 400:
# ?price=abc  →  {"price": ["A valid integer is required."]}
# ?rating=xyz →  {"rating": ["A valid number is required."]}

Built-in Validators

from restflow.filters import StringField, IntegerField

class ProductFilterSet(FilterSet):
    # Min/max value
    price = IntegerField(min_value=0, max_value=1000000)

    # Min/max length
    sku = StringField(min_length=3, max_length=20)

    # Required
    category = StringField(required=True)

# Validation errors:
# ?price=-10      →  {"price": ["Ensure this value is >= 0."]}
# ?sku=ab         →  {"sku": ["Ensure this has at least 3 characters."]}
# (no category)   →  {"category": ["This field is required."]}

Custom Validators

from rest_framework.exceptions import ValidationError

def validate_positive_even(value):
    if value <= 0:
        raise ValidationError("Must be positive")
    if value % 2 != 0:
        raise ValidationError("Must be even")

class ProductFilterSet(FilterSet):
    batch_size = IntegerField(validators=[validate_positive_even])

# ?batch_size=-2  →  {"batch_size": ["Must be positive"]}
# ?batch_size=3   →  {"batch_size": ["Must be even"]}

Choice Validation

class ProductFilterSet(FilterSet):
    status = ChoiceField(
        choices=[('draft', 'Draft'), ('published', 'Published')]
    )

# ?status=invalid  →  {"status": ["\"invalid\" is not a valid choice."]}

List Validation

class ProductFilterSet(FilterSet):
    tags = ListField(
        child=IntegerField(min_value=1),
        min_length=1,
        max_length=10
    )

# ?tags=           →  {"tags": ["This list may not be empty."]}
# ?tags=1,2,...,20 →  {"tags": ["Ensure this list has at most 10 elements."]}
# ?tags=0,1        →  {"tags": {"0": ["Ensure >= 1"]}}

PostgreSQL Fields

from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank

class ProductFilterSet(FilterSet):
    search = StringField(method="filter_fulltext")

    def filter_fulltext(self, filterset, queryset, value):
        vector = SearchVector('name', weight='A') + \
                 SearchVector('description', weight='B')
        query = SearchQuery(value)

        return queryset.annotate(
            search=vector,
            rank=SearchRank(vector, query)
        ).filter(search=query).order_by('-rank')

# Usage:
# ?search=wireless headphones

Weighted Search:

def filter_weighted_search(self, filterset, queryset, value):
    # Weight: A (highest) > B > C > D (lowest)
    vector = \
        SearchVector('title', weight='A') + \
        SearchVector('description', weight='B') + \
        SearchVector('tags', weight='C')

    query = SearchQuery(value)

    return queryset.annotate(
        rank=SearchRank(vector, query)
    ).filter(search=vector_for_filter).order_by('-rank')

Search Configuration:

from django.contrib.postgres.search import SearchVector

# English search configuration
vector = SearchVector('description', config='english')

# Different languages
vector = SearchVector('description', config='spanish')

Array Fields

from django.contrib.postgres.fields import ArrayField

# Model
class Product(models.Model):
    tags = ArrayField(models.CharField(max_length=50))
    sizes = ArrayField(models.CharField(max_length=10))

# FilterSet
class ProductFilterSet(FilterSet):
    # Basic array filtering
    tags = ListField(
        child=StringField(),
        lookups=["pg_array"]
    )

# Available lookups:
# ?tags__contains=wireless        # Array contains value
# ?tags__overlap=a,b,c           # Array overlaps with list
# ?tags__contained_by=a,b,c,d    # Array is subset of list

Array Operations:

# Contains (array must contain ALL values)
?tags__contains=wireless

# Overlaps (array has ANY of these values)
?tags__overlap=wireless,bluetooth

# Contained by (array is subset of these values)
?tags__contained_by=wireless,bluetooth,usb,hdmi

All Tags (AND logic):

class ProductFilterSet(FilterSet):
    all_tags = ListField(child=StringField(), method="filter_all_tags")

    def filter_all_tags(self, filterset, queryset, value):
        """Product must have ALL specified tags"""
        q = Q()
        for tag in value:
            q &= Q(tags__contains=[tag])
        return q

# ?all_tags=wireless,bluetooth  # Must have BOTH

JSON Fields

from django.db import models

# Model
class Product(models.Model):
    metadata = models.JSONField()
    # Example: {"brand": "Apple", "specs": {"color": "red", "size": "large"}}

# FilterSet
class ProductFilterSet(FilterSet):
    # Filter by JSON keys
    brand = StringField(lookup_expr="metadata__brand")

    # Nested JSON keys
    color = StringField(lookup_expr="metadata__specs__color")
    size = StringField(lookup_expr="metadata__specs__size")

    # JSON contains
    has_spec = StringField(lookup_expr="metadata__has_key")

# Usage:
# ?brand=Apple
# ?color=red
# ?has_spec=warranty

JSON Lookups:

# Has key
?metadata__has_key=warranty

# Has all keys
?metadata__has_keys=warranty,certificate

# Has any keys
?metadata__has_any_keys=warranty,certificate

# Contains (exact match)
?metadata__contains={"brand": "Apple"}

Range Fields (PostgreSQL)

from django.contrib.postgres.fields import IntegerRangeField, DateRangeField

# Model
class Product(models.Model):
    price_range = IntegerRangeField()
    available_dates = DateRangeField()

# FilterSet
class ProductFilterSet(FilterSet):
    # Contains value
    min_price = IntegerField(method="filter_price_contains")

    def filter_price_contains(self, filterset, queryset, value):
        return queryset.filter(price_range__contains=value)

# ?min_price=100  # Products with price range containing 100

Trigram Similarity (PostgreSQL)

from django.contrib.postgres.search import TrigramSimilarity

class ProductFilterSet(FilterSet):
    fuzzy_name = StringField(method="filter_fuzzy_name")

    def filter_fuzzy_name(self, filterset, queryset, value):
        return queryset.annotate(
            similarity=TrigramSimilarity('name', value)
        ).filter(similarity__gt=0.3).order_by('-similarity')

# ?fuzzy_name=lapto  # Finds "laptop" with typo

Field Parameters Reference

Common Parameters (All Fields)

Field(
    # Lookup configuration
    lookups=[...],              # List of lookup expressions to generate
    lookup_expr="...",          # Custom Django ORM lookup expression

    # Validation
    required=False,             # Make field required
    allow_null=False,           # Allow null values
    validators=[...],           # List of validator functions

    # Documentation
    help_text="...",            # Field description
    label="...",                # Field label

    # Custom filtering
    method="method_name",       # Custom filter method
)

StringField Parameters

StringField(
    min_length=None,            # Minimum string length
    max_length=None,            # Maximum string length
    trim_whitespace=True,       # Remove leading/trailing whitespace
    allow_blank=False,          # Allow empty strings

    # Common parameters
    lookups, lookup_expr, required, validators, help_text, method
)

IntegerField / FloatField / DecimalField Parameters

IntegerField(
    min_value=None,             # Minimum value
    max_value=None,             # Maximum value

    # Common parameters
    lookups, lookup_expr, required, validators, help_text, method
)

DecimalField(
    max_digits=None,            # Total digits (required)
    decimal_places=None,        # Decimal places (required)
    min_value=None,
    max_value=None,
    # ...
)

DateField / DateTimeField / TimeField Parameters

DateTimeField(
    input_formats=None,         # List of accepted input formats
    default_timezone=None,      # Timezone for naive datetimes
    format=None,                # Output format

    # Common parameters
    lookups, lookup_expr, required, validators, help_text, method
)

ChoiceField / MultipleChoiceField Parameters

ChoiceField(
    choices=[...],              # List of (value, label) tuples (required)
    allow_blank=False,          # Allow empty selection

    # Common parameters
    lookup_expr, required, help_text, method
)

ListField Parameters

ListField(
    child=Field(),              # Child field type (required)
    min_length=None,            # Minimum list length
    max_length=None,            # Maximum list length
    allow_empty=True,           # Allow empty list

    # Common parameters
    lookups, lookup_expr, required, help_text, method
)

Important Caveats

Custom Method with Fields

When using method parameter, return Q objects for operator compatibility:

class ProductFilterSet(FilterSet):
    in_stock = BooleanField(method="filter_in_stock")

    class Meta:
        operator = "OR"

    # ✅ CORRECT - Returns Q object
    def filter_in_stock(self, filterset, queryset, value):
        if value:
            return Q(inventory__gt=0)
        return Q()

    # ❌ WRONG - Returns QuerySet (operator ignored)
    def filter_in_stock_wrong(self, filterset, queryset, value):
        if value:
            return queryset.filter(inventory__gt=0)
        return queryset

See FilterSet guide for details.

Type Annotation Limitations

Type annotations can't express all configurations:

# ❌ Can't add validation with type annotation
price: int  # No min/max

# ✅ Use explicit declaration
price = IntegerField(min_value=0, max_value=1000000)

List Field Caveats

Comma-separated vs Multiple Parameters:

tags: List[int]

# Both work:
?tags=1,2,3           # Comma-separated
?tags=1&tags=2&tags=3 # Multiple parameters

# But behave differently with some backends

Empty Lists:

# ?tags=  → Empty string, not empty list!
# Validation error: "A valid integer is required."

# Solution: Use allow_empty=True and handle in method
tags = ListField(child=IntegerField(), allow_empty=True, method="filter_tags")

def filter_tags(self, filterset, queryset, value):
    if not value:  # Empty list
        return Q()
    return Q(tags__id__in=value)

Negation Edge Cases

Double Negation:

# ⚠️ Watch out for logic
?price__gte!=1000  # NOT (price >= 1000) → price < 1000

# Equivalent to:
?price__lt=1000

Null Checks:

# Check if null
?field__isnull=true

# Check if NOT null
?field__isnull!=true  # or ?field__isnull=false

Best Practices

Use Type Annotations for Simple Fields

# Clean and readable
class ProductFilterSet(FilterSet):
    name: str
    price: int
    in_stock: bool

Use Explicit Declarations for Complex Fields

# Clear and configurable
class ProductFilterSet(FilterSet):
    name = StringField(
        lookups=["icontains", "istartswith"],
        min_length=2,
        help_text="Product name search"
    )

Use Lookup Categories

# Concise
price = IntegerField(lookups=["comparison"])

# Verbose
price = IntegerField(lookups=["gt", "gte", "lt", "lte"])

Use extra_kwargs for Model Fields

# Maintainable
class Meta:
    model = Product
    fields = ['name', 'price']
    extra_kwargs = {
        'name': {'lookups': ['icontains']},
        'price': {'min_value': 0}
    }

Add Validation

# Validate early
price = IntegerField(min_value=0, max_value=1000000)
sku = StringField(min_length=3, max_length=20, required=True)

Document Fields

class ProductFilterSet(FilterSet):
    search = StringField(
        method="filter_search",
        help_text="Search in name, description, and tags"
    )
    min_price = IntegerField(
        lookup_expr="price__gte",
        min_value=0,
        help_text="Minimum price filter"
    )

8. Use PostgreSQL Features When Available

# Use full-text search instead of icontains
search = StringField(method="filter_fulltext")

# Use array fields for tags
tags = ListField(child=StringField(), lookups=["pg_array"])

Next Steps

  • FilterSet - Complete FilterSet guide with custom methods, operators, preprocessors, postprocessors, and performance optimization
  • Filtering Tutorial - Step-by-step tutorial covering all filtering concepts