Skip to content

Filters

Filter, sorting, search, pagination and field-projection models, plus the resolvers that turn them into backend queries.

For a complete walkthrough including resolver usage see Filters.

Exports

fastapi_views.filters re-exports:

Name Kind
BaseFilter, ModelFilter, Filter filter models
BasePaginationFilter, PaginationFilter, OffsetLimitFilter, CursorPaginationFilter pagination filters
OrderingFilter, SearchFilter, FieldsFilter sorting / search / projection filters
FilterDepends, NestedFilter FastAPI dependency factories

Three modules are not re-exported and must be imported from their submodule:

  • fastapi_views.filters.operationsFieldOperation, FilterOperation, SortOperation, LogicalOperation, Operation
  • fastapi_views.filters.typesQueryField, SearchQuery, Sort, Fields, AnyFields, plus the QueryParam wrapper and its unwrap_query_params / set_query_param helpers
  • fastapi_views.filters.resolvers.*FilterResolver (.abc), ObjectFilterResolver (.objects), SQLAlchemyFilterResolver (.sqlalchemy). The resolvers package itself is empty, so that importing filters never imports SQLAlchemy.

Filter models

BaseFilter is a Pydantic BaseModel that exposes its state as operations. Every subclass contributes to two class variables which are merged across the whole MRO in __init_subclass__: special_fields (fields that are not data filters and are excluded from as_kwargs()) and, for pagination filters, pagination_fields (which is merged into special_fields).

Class Adds Query parameters Defaults
BaseFilter filters / get_filters(), as_kwargs(), with_kwargs()
ModelFilter operations built from its own fields, field_names one per declared field per field
OrderingFilter ordering_fields, order_by / get_order_by() sort (repeatable) None
SearchFilter search_fields q (field name query) None
FieldsFilter fields_from, get_fields() fields (repeatable set) None
BasePaginationFilter pagination_fields, get_pagination()
PaginationFilter page, page_size 1, 100
OffsetLimitFilter offset, limit 0, 100
CursorPaginationFilter cursor, page_size None, 100
Filter all of the above page, page_size, sort, q, fields + own fields

Filter inherits from PaginationFilter, OrderingFilter, SearchFilter, FieldsFilter and ModelFilter, in that order.

ModelFilter derives an operation's operator from the segment after the last __ in the field name (user__name__gt → field user__name, operator gt) and uses eq only for names without any __, so a nested equality lookup must be written user__name__eq. BaseFilter.__pydantic_init_subclass__ unwraps the QueryParam metadata of every field, which is what makes query-backed fields ordinary None-defaulted pydantic fields. FieldsFilter.fields_from narrows the annotation on the declaring subclass only.

page/page_size use the PageNumber / PageSize aliases and cursor uses Cursor from fastapi_views.pagination, which also documents the page container each pagination filter pairs with.

Filter

Bases: PaginationFilter, OrderingFilter, SearchFilter, FieldsFilter, ModelFilter

Main filter class that implements all the functionalities: pagination, ordering, search, fields and custom attributes filter

Source code in fastapi_views/filters/models.py
class Filter(
    PaginationFilter,
    OrderingFilter,
    SearchFilter,
    FieldsFilter,
    ModelFilter,
):
    """Main filter class that implements all the functionalities:
    pagination, ordering, search, fields and custom attributes filter
    """

Operations

get_filters() returns a list of these dataclasses; get_order_by() returns SortOperations. set_prefix() is what NestedFilter uses to namespace a nested filter's fields as prefix__field.

Class Fields
FieldOperation field; base class providing set_prefix(prefix)
FilterOperation field, operator, values
SortOperation field, desc = False
LogicalOperation operator ("and" / "or"), values (list of operations)

Operation is the union FilterOperation | SortOperation | LogicalOperation.


Query parameter types

Annotated aliases that make a Pydantic field behave as a FastAPI query parameter. Each carries Field(None) as the pydantic default plus a fastapi.Query wrapped in QueryParam, so pydantic does not absorb the parameter definition; BaseFilter.__pydantic_init_subclass__ calls unwrap_query_params to put the real Query back into the built field's metadata, where FastAPI finds it. Consequences: the runtime default is a plain None, re-declaring = None is harmless, and several such fields in one filter stay independent parameters. A list-typed field declared without one of these aliases is inferred as a request body.

Alias Type
QueryField[T] Annotated[T \| None, Field(None), QueryParam(Query())]
SearchQuery str \| None, aliased to q
Sort list[str] \| None
Fields[T] set[T] \| None
AnyFields Fields[str]

set_query_param replaces an existing QueryParam / Query in a field's metadata — OrderingFilter uses it to give each subclass its own sort description without touching the base class.

QueryParam dataclass

Carries a fastapi.Query inside the annotation metadata.

Pydantic absorbs any FieldInfo found in Annotated[...] metadata, which would leave nothing for FastAPI to detect. Wrapping it keeps the parameter definition opaque to pydantic until unwrap_query_params puts it back into the metadata of the already built field.

Source code in fastapi_views/filters/types.py
@dataclass(frozen=True, eq=False)
class QueryParam:
    """Carries a `fastapi.Query` inside the annotation metadata.

    Pydantic absorbs any `FieldInfo` found in `Annotated[...]` metadata, which
    would leave nothing for FastAPI to detect. Wrapping it keeps the parameter
    definition opaque to pydantic until `unwrap_query_params` puts it back into
    the metadata of the already built field.
    """

    param: params.Query

Dependencies

FilterDepends and NestedFilter are FastAPI dependency factories used to inject filter instances into view methods. FilterDepends re-raises Pydantic ValidationError as RequestValidationError so invalid query parameters yield 422 instead of 500. NestedFilter(model, prefix=...) additionally applies an alias generator that renames the nested query parameters to prefix__field.


Resolvers

A resolver translates a filter into a data-layer query. apply_filter(filter, queryset, exclude=None, **context) is the entry point; it runs apply_base_filter, then apply_fields_filter, apply_ordering_filter and apply_pagination_filter for the filter bases that apply, skipping any step named in exclude ("filter", "fields", "sort", "paginate") and forwarding context to each step. Note the abstract steps take (queryset, filter) while apply_filter takes (filter, queryset).

Operator support per resolver:

Operator SQLAlchemyFilterResolver ObjectFilterResolver
eq, ne, lt, le, gt, ge yes yes
in, not_in yes no
is_null yes yes
like, ilike yes, value wrapped in %…% with \, %, _ escaped substring test (ilike lowercases both sides)
and, or (LogicalOperation) operator.and_ / or_ reduced over the values all() / any()

ObjectFilterResolver defines only is_null, like and ilike explicitly and falls back to getattr(operator, name), so any stdlib operator function name works while in / not_in raise AttributeError.

FilterResolver

ObjectFilterResolver

Filters a list in memory. getter is the accessor factory (operator.attrgetter by default; pass operator.itemgetter for dicts). Pagination slices the list and supports OffsetLimitFilter and PaginationFilter only — CursorPaginationFilter raises NotImplementedError. apply_fields_filter returns a new list in which every element is projected down to only the attributes named in ?fields: objects become shallow copies with the other entries removed from their __dict__, mappings become new dicts, and values with no __dict__ (tuples, __slots__ classes) pass through unchanged. The input list and its objects are never mutated, and an empty ?fields returns the queryset itself.

SQLAlchemyFilterResolver

Subclass it and set filter_model to the mapped class the filter's unprefixed fields belong to; it needs no constructor arguments, so Depends() can instantiate it. SQLAlchemy itself is optional — the module imports without it and raises NotImplementedError when used.

Member Description
filter_model mapped class used for unprefixed fields
operators operator name → callable mapping
resolve(operation, **context) one operation → SQLAlchemy expression
resolve_model_field(field, **context) field or prefix__field (split at the first __) → column; context["table"] overrides the base model, context[prefix]["table"] resolves a prefix, otherwise the prefix is looked up in the mapper registry by __tablename__
_cache / _get_model_cache(registry) registry → {tablename: model} memo, a WeakKeyDictionary created lazily per resolver subclass, so lookups are cached per registry and per class and cannot leak across two bases sharing a __tablename__
get_filters(filter, **context) list of WHERE expressions
get_order_by(filter, extra=None, **context) list of ORDER BY expressions, extra appended
apply_fields_filter load_only() for top-level fields, chained defaultload(...).load_only(...) for relation__field paths
apply_cursor_pagination(queryset, page, page_size, **context) raises NotImplementedError; override for keyset pagination, keeping **contextapply_pagination_filter forwards the resolver context

Column and _Queryset are typing protocols. Column documents the SQLAlchemy column methods the resolver calls — its unbound methods are used as the in, not_in, is_null, like and ilike implementations, with the real column passed as self. _Queryset is the minimal queryset surface: filter(), options(), order_by(), offset(), limit().

Column

Bases: Protocol

This is sqlalchemy.Column protocol, the real Column instance is injected as self parameter

Source code in fastapi_views/filters/resolvers/sqlalchemy.py
class Column(Protocol):
    """This is sqlalchemy.Column protocol, the real Column instance is injected as `self` parameter"""

    def in_(self, values: Sequence[Any]) -> Any:
        return self.in_(values)

    def not_in_(self, values: Sequence[Any]) -> Any:
        return self.not_in_(values)

    def is_(self, value: Any) -> Any:
        return self.is_(value)  # pragma: no cover

    def is_not(self, value: Any) -> Any:
        return self.is_not(value)  # pragma: no cover

    def is_null(self, value: bool) -> Any:  # noqa: FBT001
        return self.is_(None) if value else self.is_not(None)

    def like(self, value: str, escape: str = "\\") -> Any:
        return self.like(f"%{_escape_like_value(value)}%", escape=escape)

    def ilike(self, value: str, escape: str = "\\") -> Any:
        return self.ilike(f"%{_escape_like_value(value)}%", escape=escape)