Skip to content

API Reference

This page documents every public class, function, and exception. It is generated in part from the source docstrings via mkdocstrings — the same text you'll see in your editor's tooltips.

Serializers

PartialFieldsSerializerMixin

Mixin adding sparse-fieldset support to a DRF serializer.

Combine with :class:rest_framework.serializers.Serializer or :class:rest_framework.serializers.ModelSerializer (this mixin must come first in the MRO). Works correctly at arbitrary nesting depth: a serializer used as a nested field only needs this mixin itself for its own sub-selection (author(name,email)) to be honored — no manual context propagation is required.

Example

.. code-block:: python

class AuthorSerializer(PartialFieldsSerializerMixin, serializers.ModelSerializer):
    class Meta:
        model = Author
        fields = ["id", "name", "email", "bio"]


class ArticleSerializer(PartialFieldsSerializerMixin, serializers.ModelSerializer):
    author = AuthorSerializer()

    class Meta:
        model = Article
        fields = ["id", "title", "body", "author"]

A request to /articles/1/?fields=title,author(name) returns only title and author.name.

Source code in src/drf_partial_response_fields/serializers.py
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
class PartialFieldsSerializerMixin:
    """Mixin adding sparse-fieldset support to a DRF serializer.

    Combine with :class:`rest_framework.serializers.Serializer` or
    :class:`rest_framework.serializers.ModelSerializer` (this mixin must
    come first in the MRO). Works correctly at arbitrary nesting depth: a
    serializer used as a nested field only needs this mixin itself for its
    own sub-selection (``author(name,email)``) to be honored — no manual
    context propagation is required.

    Example:
        .. code-block:: python

            class AuthorSerializer(PartialFieldsSerializerMixin, serializers.ModelSerializer):
                class Meta:
                    model = Author
                    fields = ["id", "name", "email", "bio"]


            class ArticleSerializer(PartialFieldsSerializerMixin, serializers.ModelSerializer):
                author = AuthorSerializer()

                class Meta:
                    model = Article
                    fields = ["id", "title", "body", "author"]

        A request to ``/articles/1/?fields=title,author(name)`` returns
        only ``title`` and ``author.name``.
    """

    def get_fields(self) -> dict[str, Field[Any, Any, Any, Any]]:
        """Return the field set narrowed to what the client requested.

        Returns:
            An ordered mapping of field name to bound
            :class:`~rest_framework.fields.Field` instance, restricted
            according to the :class:`~drf_partial_response_fields.tree.FieldTree`
            resolved for this serializer's position in the response tree.
        """
        fields: dict[str, Field[Any, Any, Any, Any]] = super().get_fields()  # type: ignore[misc]
        tree = self._resolve_local_tree()
        self._partial_response_alias_map = _build_alias_map(tree)

        if tree.mode == "all":
            return fields

        strict = get_setting("STRICT")
        always_include = set(get_setting("ALWAYS_INCLUDE"))
        allowed = resolve_allowed_names(tree, fields.keys(), strict=strict)
        allowed |= always_include & fields.keys()
        return OrderedDict((name, f) for name, f in fields.items() if name in allowed)

    def to_representation(self, instance: Any) -> Any:
        """Serialize ``instance``, applying any requested field aliases.

        Args:
            instance: The object being serialized.

        Returns:
            The representation produced by the parent class, with any
            ``alias:field`` renames from the request applied to its keys.
        """
        ret = super().to_representation(instance)  # type: ignore[misc]
        alias_map = getattr(self, "_partial_response_alias_map", None)
        if alias_map:
            renamed = OrderedDict()
            for key, value in ret.items():
                renamed[alias_map.get(key, key)] = value
            return renamed
        return ret

    def _resolve_local_tree(self) -> FieldTree:
        """Resolve the :class:`FieldTree` that applies to this serializer.

        Walks from this serializer instance up to the root of the
        serializer tree to determine this instance's dotted path (e.g.
        ``["author"]`` for a serializer nested under an ``author`` field),
        then descends into the request-wide tree stored in the root's
        context following that path.

        Returns:
            The resolved :class:`~drf_partial_response_fields.tree.FieldTree`
            for this exact position in the response, or
            :data:`~drf_partial_response_fields.tree.ALL_TREE` if no
            restriction applies (no ``fields`` parameter was supplied, or
            this position was not restricted by a parent group).
        """
        context = self.context  # type: ignore[attr-defined]
        top_tree: FieldTree | None = context.get(CONTEXT_KEY) if context else None
        if top_tree is None:
            return ALL_TREE

        node: FieldTree = top_tree
        for name in self._path_from_root():
            node = child_tree(node, name)
            if node is ALL_TREE:
                return ALL_TREE
        return node

    def _path_from_root(self) -> list[str]:
        """Compute the field-name path from the root serializer to here."""
        segments: list[str] = []
        node: Any = self
        while node.parent is not None:
            name = node.field_name
            if name:
                segments.append(name)
            node = node.parent
        segments.reverse()
        return segments

get_fields()

Return the field set narrowed to what the client requested.

Returns:

Type Description
dict[str, Field[Any, Any, Any, Any]]

An ordered mapping of field name to bound

dict[str, Field[Any, Any, Any, Any]]

class:~rest_framework.fields.Field instance, restricted

dict[str, Field[Any, Any, Any, Any]]

according to the :class:~drf_partial_response_fields.tree.FieldTree

dict[str, Field[Any, Any, Any, Any]]

resolved for this serializer's position in the response tree.

Source code in src/drf_partial_response_fields/serializers.py
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
def get_fields(self) -> dict[str, Field[Any, Any, Any, Any]]:
    """Return the field set narrowed to what the client requested.

    Returns:
        An ordered mapping of field name to bound
        :class:`~rest_framework.fields.Field` instance, restricted
        according to the :class:`~drf_partial_response_fields.tree.FieldTree`
        resolved for this serializer's position in the response tree.
    """
    fields: dict[str, Field[Any, Any, Any, Any]] = super().get_fields()  # type: ignore[misc]
    tree = self._resolve_local_tree()
    self._partial_response_alias_map = _build_alias_map(tree)

    if tree.mode == "all":
        return fields

    strict = get_setting("STRICT")
    always_include = set(get_setting("ALWAYS_INCLUDE"))
    allowed = resolve_allowed_names(tree, fields.keys(), strict=strict)
    allowed |= always_include & fields.keys()
    return OrderedDict((name, f) for name, f in fields.items() if name in allowed)

to_representation(instance)

Serialize instance, applying any requested field aliases.

Parameters:

Name Type Description Default
instance Any

The object being serialized.

required

Returns:

Type Description
Any

The representation produced by the parent class, with any

Any

alias:field renames from the request applied to its keys.

Source code in src/drf_partial_response_fields/serializers.py
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def to_representation(self, instance: Any) -> Any:
    """Serialize ``instance``, applying any requested field aliases.

    Args:
        instance: The object being serialized.

    Returns:
        The representation produced by the parent class, with any
        ``alias:field`` renames from the request applied to its keys.
    """
    ret = super().to_representation(instance)  # type: ignore[misc]
    alias_map = getattr(self, "_partial_response_alias_map", None)
    if alias_map:
        renamed = OrderedDict()
        for key, value in ret.items():
            renamed[alias_map.get(key, key)] = value
        return renamed
    return ret

PartialFieldsSerializer

Bases: PartialFieldsSerializerMixin, Serializer[Any]

Convenience base combining :class:PartialFieldsSerializerMixin with :class:rest_framework.serializers.Serializer, for non-model serializers.

Source code in src/drf_partial_response_fields/serializers.py
149
150
151
152
class PartialFieldsSerializer(PartialFieldsSerializerMixin, serializers.Serializer[Any]):
    """Convenience base combining :class:`PartialFieldsSerializerMixin` with
    :class:`rest_framework.serializers.Serializer`, for non-model serializers.
    """

PartialFieldsModelSerializer

Bases: PartialFieldsSerializerMixin, ModelSerializer[Any]

Convenience base combining :class:PartialFieldsSerializerMixin with :class:rest_framework.serializers.ModelSerializer.

Source code in src/drf_partial_response_fields/serializers.py
155
156
157
158
class PartialFieldsModelSerializer(PartialFieldsSerializerMixin, serializers.ModelSerializer[Any]):
    """Convenience base combining :class:`PartialFieldsSerializerMixin` with
    :class:`rest_framework.serializers.ModelSerializer`.
    """

PartialFieldsListSerializer

Bases: ListSerializer[Any]

A :class:~rest_framework.serializers.ListSerializer variant that is a no-op subclass kept for symmetry and forward compatibility.

Field filtering for many=True serializers is handled entirely by :class:PartialFieldsSerializerMixin on the child serializer (its _path_from_root correctly skips the list wrapper), so this class does not need to override any behavior today. It exists so that users who explicitly set Meta.list_serializer_class have a documented, stable name to point to.

Source code in src/drf_partial_response_fields/serializers.py
161
162
163
164
165
166
167
168
169
170
171
class PartialFieldsListSerializer(ListSerializer[Any]):
    """A :class:`~rest_framework.serializers.ListSerializer` variant that is
    a no-op subclass kept for symmetry and forward compatibility.

    Field filtering for ``many=True`` serializers is handled entirely by
    :class:`PartialFieldsSerializerMixin` on the child serializer (its
    ``_path_from_root`` correctly skips the list wrapper), so this class
    does not need to override any behavior today. It exists so that users
    who explicitly set ``Meta.list_serializer_class`` have a documented,
    stable name to point to.
    """

Views

PartialResponseMixin

Adds ?fields= support and automatic query optimization to a view.

Mix this into any :class:~rest_framework.generics.GenericAPIView subclass — including every generic view (ListAPIView, RetrieveUpdateDestroyAPIView, ...) and every viewset (ModelViewSet, ReadOnlyModelViewSet, custom GenericViewSet subclasses) — before the DRF base class in the MRO:

.. code-block:: python

class ArticleViewSet(PartialResponseMixin, viewsets.ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

This mixin does two things:

  1. Injects the parsed fields request into the serializer context (via :meth:get_serializer_context), which :class:~drf_partial_response_fields.serializers.PartialFieldsSerializerMixin reads to filter fields.
  2. Rewrites the queryset (via :meth:get_queryset) to add select_related, prefetch_related, and only so that only the data needed for the requested fields is fetched — see :func:drf_partial_response_fields.optimizer.optimize_queryset.

Both behaviors compose transparently with pagination: optimization happens on the unsliced queryset before the paginator runs, so page size and query cost stay independent of which fields were requested.

Source code in src/drf_partial_response_fields/mixins.py
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
class PartialResponseMixin:
    """Adds ``?fields=`` support and automatic query optimization to a view.

    Mix this into any :class:`~rest_framework.generics.GenericAPIView`
    subclass — including every generic view (``ListAPIView``,
    ``RetrieveUpdateDestroyAPIView``, ...) and every viewset
    (``ModelViewSet``, ``ReadOnlyModelViewSet``, custom
    ``GenericViewSet`` subclasses) — *before* the DRF base class in the
    MRO:

    .. code-block:: python

        class ArticleViewSet(PartialResponseMixin, viewsets.ModelViewSet):
            queryset = Article.objects.all()
            serializer_class = ArticleSerializer

    This mixin does two things:

    1. Injects the parsed ``fields`` request into the serializer context
       (via :meth:`get_serializer_context`), which
       :class:`~drf_partial_response_fields.serializers.PartialFieldsSerializerMixin`
       reads to filter fields.
    2. Rewrites the queryset (via :meth:`get_queryset`) to add
       ``select_related``, ``prefetch_related``, and ``only`` so that only
       the data needed for the requested fields is fetched — see
       :func:`drf_partial_response_fields.optimizer.optimize_queryset`.

    Both behaviors compose transparently with pagination: optimization
    happens on the unsliced queryset before the paginator runs, so page
    size and query cost stay independent of which fields were requested.
    """

    request: Request
    kwargs: dict[str, Any]

    def get_serializer_context(self) -> dict[str, Any]:
        """Return the serializer context with the parsed fields tree attached.

        Returns:
            The context dict produced by the next class in the MRO (e.g.
            ``GenericAPIView.get_serializer_context``), with the parsed
            :class:`~drf_partial_response_fields.tree.FieldTree` added
            under
            :data:`~drf_partial_response_fields.constants.CONTEXT_KEY`.
        """
        context: dict[str, Any] = super().get_serializer_context()  # type: ignore[misc]
        context[CONTEXT_KEY] = self.get_partial_response_fields_tree()
        return context

    def get_partial_response_fields_tree(self) -> FieldTree:
        """Return (and cache) the parsed fields tree for the current request.

        When the :ref:`SAFE_METHODS_ONLY <settings-safe-methods-only>`
        setting is enabled (the default), this returns
        :data:`~drf_partial_response_fields.tree.ALL_TREE` for any request
        whose method is not ``GET``, ``HEAD``, or ``OPTIONS`` — see the
        setting's docstring for why this matters for write endpoints.

        Returns:
            The :class:`~drf_partial_response_fields.tree.FieldTree` parsed
            from this request's ``fields`` query parameter. The result is
            cached on the view instance for the lifetime of the request,
            since a view instance handles exactly one request in DRF.
        """
        if not hasattr(self, _CACHE_ATTR):
            if get_setting("SAFE_METHODS_ONLY") and self.request.method not in SAFE_METHODS:
                tree = ALL_TREE
            else:
                tree = parse_request_fields(self.request)
            setattr(self, _CACHE_ATTR, tree)
        cached: FieldTree = getattr(self, _CACHE_ATTR)
        return cached

    def get_queryset(self) -> Any:
        """Return the view's queryset, optimized for the requested fields.

        Returns:
            The queryset produced by the next class in the MRO (e.g.
            ``GenericAPIView.get_queryset``), passed through
            :func:`~drf_partial_response_fields.optimizer.optimize_queryset`
            when the view's serializer is a
            :class:`~rest_framework.serializers.ModelSerializer` subclass
            and query optimization is enabled. Non-model serializers, or
            optimization disabled via the ``ENABLE_QUERY_OPTIMIZATION``
            setting, leave the queryset untouched.
        """
        queryset = super().get_queryset()  # type: ignore[misc]
        serializer_class = self.get_serializer_class()  # type: ignore[attr-defined]
        if not (
            isinstance(serializer_class, type) and issubclass(serializer_class, ModelSerializer)
        ):
            return queryset
        tree = self.get_partial_response_fields_tree()
        return optimize_queryset(queryset, serializer_class, tree)

get_partial_response_fields_tree()

Return (and cache) the parsed fields tree for the current request.

When the :ref:SAFE_METHODS_ONLY <settings-safe-methods-only> setting is enabled (the default), this returns :data:~drf_partial_response_fields.tree.ALL_TREE for any request whose method is not GET, HEAD, or OPTIONS — see the setting's docstring for why this matters for write endpoints.

Returns:

Name Type Description
The FieldTree

class:~drf_partial_response_fields.tree.FieldTree parsed

FieldTree

from this request's fields query parameter. The result is

FieldTree

cached on the view instance for the lifetime of the request,

FieldTree

since a view instance handles exactly one request in DRF.

Source code in src/drf_partial_response_fields/mixins.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def get_partial_response_fields_tree(self) -> FieldTree:
    """Return (and cache) the parsed fields tree for the current request.

    When the :ref:`SAFE_METHODS_ONLY <settings-safe-methods-only>`
    setting is enabled (the default), this returns
    :data:`~drf_partial_response_fields.tree.ALL_TREE` for any request
    whose method is not ``GET``, ``HEAD``, or ``OPTIONS`` — see the
    setting's docstring for why this matters for write endpoints.

    Returns:
        The :class:`~drf_partial_response_fields.tree.FieldTree` parsed
        from this request's ``fields`` query parameter. The result is
        cached on the view instance for the lifetime of the request,
        since a view instance handles exactly one request in DRF.
    """
    if not hasattr(self, _CACHE_ATTR):
        if get_setting("SAFE_METHODS_ONLY") and self.request.method not in SAFE_METHODS:
            tree = ALL_TREE
        else:
            tree = parse_request_fields(self.request)
        setattr(self, _CACHE_ATTR, tree)
    cached: FieldTree = getattr(self, _CACHE_ATTR)
    return cached

get_queryset()

Return the view's queryset, optimized for the requested fields.

Returns:

Type Description
Any

The queryset produced by the next class in the MRO (e.g.

Any

GenericAPIView.get_queryset), passed through

Any

func:~drf_partial_response_fields.optimizer.optimize_queryset

Any

when the view's serializer is a

Any

class:~rest_framework.serializers.ModelSerializer subclass

Any

and query optimization is enabled. Non-model serializers, or

Any

optimization disabled via the ENABLE_QUERY_OPTIMIZATION

Any

setting, leave the queryset untouched.

Source code in src/drf_partial_response_fields/mixins.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
def get_queryset(self) -> Any:
    """Return the view's queryset, optimized for the requested fields.

    Returns:
        The queryset produced by the next class in the MRO (e.g.
        ``GenericAPIView.get_queryset``), passed through
        :func:`~drf_partial_response_fields.optimizer.optimize_queryset`
        when the view's serializer is a
        :class:`~rest_framework.serializers.ModelSerializer` subclass
        and query optimization is enabled. Non-model serializers, or
        optimization disabled via the ``ENABLE_QUERY_OPTIMIZATION``
        setting, leave the queryset untouched.
    """
    queryset = super().get_queryset()  # type: ignore[misc]
    serializer_class = self.get_serializer_class()  # type: ignore[attr-defined]
    if not (
        isinstance(serializer_class, type) and issubclass(serializer_class, ModelSerializer)
    ):
        return queryset
    tree = self.get_partial_response_fields_tree()
    return optimize_queryset(queryset, serializer_class, tree)

get_serializer_context()

Return the serializer context with the parsed fields tree attached.

Returns:

Type Description
dict[str, Any]

The context dict produced by the next class in the MRO (e.g.

dict[str, Any]

GenericAPIView.get_serializer_context), with the parsed

dict[str, Any]

class:~drf_partial_response_fields.tree.FieldTree added

dict[str, Any]

under

dict[str, Any]

data:~drf_partial_response_fields.constants.CONTEXT_KEY.

Source code in src/drf_partial_response_fields/mixins.py
118
119
120
121
122
123
124
125
126
127
128
129
130
def get_serializer_context(self) -> dict[str, Any]:
    """Return the serializer context with the parsed fields tree attached.

    Returns:
        The context dict produced by the next class in the MRO (e.g.
        ``GenericAPIView.get_serializer_context``), with the parsed
        :class:`~drf_partial_response_fields.tree.FieldTree` added
        under
        :data:`~drf_partial_response_fields.constants.CONTEXT_KEY`.
    """
    context: dict[str, Any] = super().get_serializer_context()  # type: ignore[misc]
    context[CONTEXT_KEY] = self.get_partial_response_fields_tree()
    return context

parse_request_fields

Parse the fields query parameter off an in-flight request.

Use this directly in a plain :class:~rest_framework.views.APIView (which :class:PartialResponseMixin cannot hook into automatically) to build the context dict for a serializer by hand:

.. code-block:: python

class ArticleDetailView(APIView):
    def get(self, request, pk):
        article = get_object_or_404(Article, pk=pk)
        serializer = ArticleSerializer(
            article,
            context={
                "request": request,
                PARTIAL_RESPONSE_FIELDS_CONTEXT_KEY: parse_request_fields(request),
            },
        )
        return Response(serializer.data)

Parameters:

Name Type Description Default
request Request

The current DRF :class:~rest_framework.request.Request.

required

Returns:

Type Description
FieldTree

The parsed :class:~drf_partial_response_fields.tree.FieldTree for

FieldTree

this request, using the configured QUERY_PARAM, MAX_DEPTH,

FieldTree

and MAX_FIELDS_LENGTH settings.

Raises:

Type Description
InvalidFieldsParameterError

If the query parameter value is not syntactically valid.

Source code in src/drf_partial_response_fields/mixins.py
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
def parse_request_fields(request: Request) -> FieldTree:
    """Parse the ``fields`` query parameter off an in-flight request.

    Use this directly in a plain :class:`~rest_framework.views.APIView`
    (which :class:`PartialResponseMixin` cannot hook into automatically)
    to build the context dict for a serializer by hand:

    .. code-block:: python

        class ArticleDetailView(APIView):
            def get(self, request, pk):
                article = get_object_or_404(Article, pk=pk)
                serializer = ArticleSerializer(
                    article,
                    context={
                        "request": request,
                        PARTIAL_RESPONSE_FIELDS_CONTEXT_KEY: parse_request_fields(request),
                    },
                )
                return Response(serializer.data)

    Args:
        request: The current DRF :class:`~rest_framework.request.Request`.

    Returns:
        The parsed :class:`~drf_partial_response_fields.tree.FieldTree` for
        this request, using the configured ``QUERY_PARAM``, ``MAX_DEPTH``,
        and ``MAX_FIELDS_LENGTH`` settings.

    Raises:
        drf_partial_response_fields.exceptions.InvalidFieldsParameterError: If
            the query parameter value is not syntactically valid.
    """
    raw = request.query_params.get(get_setting("QUERY_PARAM"), "")
    return parse_fields(
        raw,
        max_depth=get_setting("MAX_DEPTH"),
        max_length=get_setting("MAX_FIELDS_LENGTH"),
    )

PARTIAL_RESPONSE_FIELDS_CONTEXT_KEY

A module-level constant: the serializer-context key under which PartialResponseMixin stores the parsed FieldTree for the current request. Public alias of drf_partial_response_fields.constants.CONTEXT_KEY, for use by manual (non-mixin) integrations such as plain APIView subclasses — see Advanced Usage.

Query optimization

optimize_queryset

Apply select_related / prefetch_related / only to a queryset.

Parameters:

Name Type Description Default
queryset QuerySet[Any]

The base queryset to optimize. It is not mutated; a new queryset is returned.

required
serializer_class type[Serializer[Any]]

The (potentially nested) serializer class that will render the queryset's results. Must ultimately be a :class:~rest_framework.serializers.ModelSerializer subclass for optimization to have any effect; other serializers are returned unmodified.

required
tree FieldTree

The parsed field restriction for the top level of the response (as produced by :func:drf_partial_response_fields.parser.parse_fields).

required
strict bool | None

Whether unknown field names should raise. Defaults to the :ref:STRICT <settings-strict> setting when None.

None
required_only_fields frozenset[str]

Field names that must be included in only() regardless of what was requested. Used internally when building the queryset for a reverse-FK Prefetch: the FK column back to the parent must never be deferred, or Django's own prefetch bucketing silently re-fetches it one row at a time.

frozenset()

Returns:

Type Description
QuerySet[Any]

A new queryset with the relevant select_related,

QuerySet[Any]

prefetch_related, and only calls applied. If the requested

QuerySet[Any]

fields include nothing optimizable, the original queryset is

QuerySet[Any]

returned unchanged.

Raises:

Type Description
UnknownFieldError

If strict (or the STRICT setting) is enabled and an unknown field name is present in tree.

Source code in src/drf_partial_response_fields/optimizer.py
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
def optimize_queryset(
    queryset: QuerySet[Any],
    serializer_class: type[Serializer[Any]],
    tree: FieldTree,
    *,
    strict: bool | None = None,
    required_only_fields: frozenset[str] = frozenset(),
) -> QuerySet[Any]:
    """Apply ``select_related`` / ``prefetch_related`` / ``only`` to a queryset.

    Args:
        queryset: The base queryset to optimize. It is not mutated;
            a new queryset is returned.
        serializer_class: The (potentially nested) serializer class that
            will render the queryset's results. Must ultimately be a
            :class:`~rest_framework.serializers.ModelSerializer` subclass
            for optimization to have any effect; other serializers are
            returned unmodified.
        tree: The parsed field restriction for the top level of the
            response (as produced by
            :func:`drf_partial_response_fields.parser.parse_fields`).
        strict: Whether unknown field names should raise. Defaults to the
            :ref:`STRICT <settings-strict>` setting when ``None``.
        required_only_fields: Field names that must be included in
            ``only()`` regardless of what was requested. Used internally
            when building the queryset for a reverse-FK ``Prefetch``: the
            FK column back to the parent must never be deferred, or
            Django's own prefetch bucketing silently re-fetches it one row
            at a time.

    Returns:
        A new queryset with the relevant ``select_related``,
        ``prefetch_related``, and ``only`` calls applied. If the requested
        fields include nothing optimizable, the original queryset is
        returned unchanged.

    Raises:
        drf_partial_response_fields.exceptions.UnknownFieldError: If
            ``strict`` (or the ``STRICT`` setting) is enabled and an
            unknown field name is present in ``tree``.
    """
    if not get_setting("ENABLE_QUERY_OPTIMIZATION"):
        return queryset
    model = getattr(queryset, "model", None)
    if model is None:
        return queryset

    acc = _Accumulator(
        only_fields=set(required_only_fields),
        strict=get_setting("STRICT") if strict is None else strict,
    )
    annotations = (
        frozenset(queryset.query.annotations) if hasattr(queryset, "query") else frozenset()
    )

    _collect_optimizations(
        serializer_class=serializer_class,
        tree=tree,
        model=model,
        prefix="",
        annotations=annotations,
        acc=acc,
    )

    optimized = queryset
    if acc.select_related:
        optimized = optimized.select_related(*sorted(acc.select_related))
    if acc.prefetch_related:
        optimized = optimized.prefetch_related(*acc.prefetch_related)
    if acc.only_fields:
        optimized = optimized.only(*sorted(acc.only_fields))
    return optimized

Decorators

Declare the relations a get_<field> method depends on.

Attach this to a :class:~rest_framework.serializers.SerializerMethodField method so that :func:drf_partial_response_fields.optimizer.optimize_queryset includes the declared relations in the generated queryset — but only when a client actually requests the field, avoiding the cost when it is not needed.

Parameters:

Name Type Description Default
select_related Sequence[str]

Dotted relation paths to add to select_related when this field is included in the response.

()
prefetch_related Sequence[str]

Dotted relation paths to add to prefetch_related when this field is included in the response.

()

Returns:

Type Description
Callable[[_F], _F]

A decorator that attaches the hints to the wrapped method and

Callable[[_F], _F]

returns it unchanged.

Example

.. code-block:: python

class ArticleSerializer(PartialFieldsSerializerMixin, serializers.ModelSerializer):
    editor_name = serializers.SerializerMethodField()

    @requires_related(select_related=["editor__profile"])
    def get_editor_name(self, obj):
        return obj.editor.profile.display_name
Source code in src/drf_partial_response_fields/decorators.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
def requires_related(
    *,
    select_related: Sequence[str] = (),
    prefetch_related: Sequence[str] = (),
) -> Callable[[_F], _F]:
    """Declare the relations a ``get_<field>`` method depends on.

    Attach this to a :class:`~rest_framework.serializers.SerializerMethodField`
    method so that :func:`drf_partial_response_fields.optimizer.optimize_queryset`
    includes the declared relations in the generated queryset — but only
    when a client actually requests the field, avoiding the cost when it is
    not needed.

    Args:
        select_related: Dotted relation paths to add to ``select_related``
            when this field is included in the response.
        prefetch_related: Dotted relation paths to add to
            ``prefetch_related`` when this field is included in the
            response.

    Returns:
        A decorator that attaches the hints to the wrapped method and
        returns it unchanged.

    Example:
        .. code-block:: python

            class ArticleSerializer(PartialFieldsSerializerMixin, serializers.ModelSerializer):
                editor_name = serializers.SerializerMethodField()

                @requires_related(select_related=["editor__profile"])
                def get_editor_name(self, obj):
                    return obj.editor.profile.display_name
    """

    def decorator(func: _F) -> _F:
        func.__dict__[HINTS_ATTR] = OptimizationHints(
            select_related=tuple(select_related),
            prefetch_related=tuple(prefetch_related),
        )
        return func

    return decorator

OptimizationHints

Extra select_related / prefetch_related paths for one field.

Attributes:

Name Type Description
select_related tuple[str, ...]

Relation paths (Django __ notation) that must be joined via select_related for this field's method to avoid extra queries.

prefetch_related tuple[str, ...]

Relation paths that must be fetched via prefetch_related for this field's method to avoid extra queries.

Source code in src/drf_partial_response_fields/decorators.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@dataclass(frozen=True, slots=True)
class OptimizationHints:
    """Extra ``select_related`` / ``prefetch_related`` paths for one field.

    Attributes:
        select_related: Relation paths (Django ``__`` notation) that must
            be joined via ``select_related`` for this field's method to
            avoid extra queries.
        prefetch_related: Relation paths that must be fetched via
            ``prefetch_related`` for this field's method to avoid extra
            queries.
    """

    select_related: tuple[str, ...] = ()
    prefetch_related: tuple[str, ...] = ()

get_hints

Return the :class:OptimizationHints attached to func, if any.

Parameters:

Name Type Description Default
func object

A bound or unbound method, typically a serializer's get_<field_name> method.

required

Returns:

Type Description
OptimizationHints | None

The attached :class:OptimizationHints, or None if func

OptimizationHints | None

was never decorated with :func:requires_related.

Source code in src/drf_partial_response_fields/decorators.py
88
89
90
91
92
93
94
95
96
97
98
99
def get_hints(func: object) -> OptimizationHints | None:
    """Return the :class:`OptimizationHints` attached to ``func``, if any.

    Args:
        func: A bound or unbound method, typically a serializer's
            ``get_<field_name>`` method.

    Returns:
        The attached :class:`OptimizationHints`, or ``None`` if ``func``
        was never decorated with :func:`requires_related`.
    """
    return getattr(func, HINTS_ATTR, None)

Parsing

parse_fields

Parse a raw fields query-parameter value into a :class:FieldTree.

Parameters:

Name Type Description Default
raw str

The raw string value of the query parameter, e.g. "id,name,author(name,email)". An empty string is treated as "no restriction".

required
max_depth int

Maximum allowed parenthesis nesting depth. Guards against pathological input designed to exhaust the parser's call stack.

6
max_length int

Maximum allowed length of raw, checked before any parsing work is done.

2000

Returns:

Type Description
FieldTree

The parsed, immutable :class:~drf_partial_response_fields.tree.FieldTree.

Raises:

Type Description
InvalidFieldsParameterError

If raw is not valid according to the grammar above, exceeds max_length, or exceeds max_depth.

Example

tree = parse_fields("id,author(name,email)") tree.mode 'include' sorted(tree.includes) ['author', 'id']

Source code in src/drf_partial_response_fields/parser.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def parse_fields(raw: str, *, max_depth: int = 6, max_length: int = 2000) -> FieldTree:
    """Parse a raw ``fields`` query-parameter value into a :class:`FieldTree`.

    Args:
        raw: The raw string value of the query parameter, e.g.
            ``"id,name,author(name,email)"``. An empty string is treated
            as "no restriction".
        max_depth: Maximum allowed parenthesis nesting depth. Guards
            against pathological input designed to exhaust the parser's
            call stack.
        max_length: Maximum allowed length of ``raw``, checked before any
            parsing work is done.

    Returns:
        The parsed, immutable :class:`~drf_partial_response_fields.tree.FieldTree`.

    Raises:
        drf_partial_response_fields.exceptions.InvalidFieldsParameterError: If
            ``raw`` is not valid according to the grammar above, exceeds
            ``max_length``, or exceeds ``max_depth``.

    Example:
        >>> tree = parse_fields("id,author(name,email)")
        >>> tree.mode
        'include'
        >>> sorted(tree.includes)
        ['author', 'id']
    """
    if not raw:
        return ALL_TREE
    if len(raw) > max_length:
        raise InvalidFieldsParameterError(
            f"The fields parameter is too long ({len(raw)} characters); "
            f"the maximum allowed length is {max_length}."
        )
    parser = _Parser(raw, max_depth=max_depth)
    tree = parser.parse_group(depth=1)
    parser.skip_whitespace()
    if parser.pos != len(raw):
        raise InvalidFieldsParameterError(
            f"Unexpected character {raw[parser.pos]!r} at position {parser.pos} "
            f"in fields expression {raw!r}."
        )
    return tree

Data model

FieldTree

An immutable tree describing which fields to include at one level.

Attributes:

Name Type Description
mode TreeMode

"all" means no restriction applies (every field available on the serializer is included); "include" means only the names in includes are kept; "exclude" means every field is kept except the names in excludes.

includes Mapping[str, FieldSpec]

Mapping of field name to :class:FieldSpec, populated only when mode == "include".

excludes frozenset[str]

The set of excluded field names, populated only when mode == "exclude".

Source code in src/drf_partial_response_fields/tree.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@dataclass(frozen=True, slots=True)
class FieldTree:
    """An immutable tree describing which fields to include at one level.

    Attributes:
        mode: ``"all"`` means no restriction applies (every field available
            on the serializer is included); ``"include"`` means only the
            names in ``includes`` are kept; ``"exclude"`` means every field
            is kept *except* the names in ``excludes``.
        includes: Mapping of field name to :class:`FieldSpec`, populated
            only when ``mode == "include"``.
        excludes: The set of excluded field names, populated only when
            ``mode == "exclude"``.
    """

    mode: TreeMode
    includes: Mapping[str, FieldSpec] = field(default_factory=dict)
    excludes: frozenset[str] = frozenset()

FieldSpec

A single requested field within a :class:FieldTree.

Attributes:

Name Type Description
name str

The concrete field name as it appears on the serializer.

alias str | None

The key the field should be renamed to in the response, or None if the field should keep its original name.

children FieldTree | None

A nested :class:FieldTree restricting which sub-fields of this field are included (only meaningful when the field is itself a nested serializer), or None if no nested restriction was specified (meaning: include the field's own default representation, unrestricted).

Source code in src/drf_partial_response_fields/tree.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@dataclass(frozen=True, slots=True)
class FieldSpec:
    """A single requested field within a :class:`FieldTree`.

    Attributes:
        name: The concrete field name as it appears on the serializer.
        alias: The key the field should be renamed to in the response, or
            ``None`` if the field should keep its original name.
        children: A nested :class:`FieldTree` restricting which sub-fields
            of this field are included (only meaningful when the field is
            itself a nested serializer), or ``None`` if no nested
            restriction was specified (meaning: include the field's own
            default representation, unrestricted).
    """

    name: str
    alias: str | None = None
    children: FieldTree | None = None

ALL_TREE

resolve_allowed_names

Compute the set of field names allowed by tree.

Parameters:

Name Type Description Default
tree FieldTree

The parsed field restriction for this level.

required
available Collection[str]

The full set of field names that exist at this level (e.g. every key in a serializer's get_fields() result).

required
strict bool

If True, raise :class:~drf_partial_response_fields.exceptions.UnknownFieldError when tree references a name that is not in available. If False, unknown names are silently ignored.

required

Returns:

Type Description
set[str]

The subset of available that should be kept.

Raises:

Type Description
UnknownFieldError

If strict is True and an unknown field name was requested.

Source code in src/drf_partial_response_fields/tree.py
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
def resolve_allowed_names(
    tree: FieldTree,
    available: Collection[str],
    *,
    strict: bool,
) -> set[str]:
    """Compute the set of field names allowed by ``tree``.

    Args:
        tree: The parsed field restriction for this level.
        available: The full set of field names that exist at this level
            (e.g. every key in a serializer's ``get_fields()`` result).
        strict: If ``True``, raise
            :class:`~drf_partial_response_fields.exceptions.UnknownFieldError`
            when ``tree`` references a name that is not in ``available``.
            If ``False``, unknown names are silently ignored.

    Returns:
        The subset of ``available`` that should be kept.

    Raises:
        drf_partial_response_fields.exceptions.UnknownFieldError: If
            ``strict`` is ``True`` and an unknown field name was requested.
    """
    available_set = set(available)
    if tree.mode == "all":
        return available_set
    if tree.mode == "exclude":
        unknown_excluded: AbstractSet[str] = tree.excludes - available_set
        if strict and unknown_excluded:
            _raise_unknown(unknown_excluded)
        return available_set - tree.excludes
    # mode == "include"
    requested = set(tree.includes)
    unknown_requested: AbstractSet[str] = requested - available_set
    if unknown_requested:
        if strict:
            _raise_unknown(unknown_requested)
        requested -= unknown_requested
    return requested

child_tree

Descend one level into tree following field name.

Parameters:

Name Type Description Default
tree FieldTree

The tree at the current level.

required
name str

The field name to descend into.

required

Returns:

Type Description
FieldTree

The nested :class:FieldTree requested for name, or

FieldTree

data:ALL_TREE if tree does not restrict name (either

FieldTree

because tree itself is unrestricted, name was requested

FieldTree

without an explicit nested group, or tree is in exclude

FieldTree

mode, which never restricts what happens within a kept field).

Source code in src/drf_partial_response_fields/tree.py
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
def child_tree(tree: FieldTree, name: str) -> FieldTree:
    """Descend one level into ``tree`` following field ``name``.

    Args:
        tree: The tree at the current level.
        name: The field name to descend into.

    Returns:
        The nested :class:`FieldTree` requested for ``name``, or
        :data:`ALL_TREE` if ``tree`` does not restrict ``name`` (either
        because ``tree`` itself is unrestricted, ``name`` was requested
        without an explicit nested group, or ``tree`` is in ``exclude``
        mode, which never restricts what happens *within* a kept field).
    """
    if tree.mode != "include":
        return ALL_TREE
    spec = tree.includes.get(name)
    if spec is None or spec.children is None:
        return ALL_TREE
    return spec.children

Exceptions

PartialResponseFieldsError

Bases: ValidationError

Base class for all errors raised by this package.

Catch this class if you want to handle any error originating from drf-partial-response-fields without depending on the more specific subclasses.

Source code in src/drf_partial_response_fields/exceptions.py
15
16
17
18
19
20
21
22
23
class PartialResponseFieldsError(_DRFValidationError):
    """Base class for all errors raised by this package.

    Catch this class if you want to handle any error originating from
    ``drf-partial-response-fields`` without depending on the more specific
    subclasses.
    """

    default_code = "partial_response_fields_error"

InvalidFieldsParameterError

Bases: PartialResponseFieldsError

Raised when the fields query parameter cannot be parsed.

This covers syntax errors such as unbalanced parentheses, empty field names, invalid characters, ambiguous combinations of inclusion and exclusion within the same group, and expressions that exceed the configured maximum nesting depth.

Parameters:

Name Type Description Default
message str

A human-readable description of the syntax error, including the offending fragment where possible.

required
Example

from drf_partial_response_fields.parser import parse_fields parse_fields("author(name") Traceback (most recent call last): ... drf_partial_response_fields.exceptions.InvalidFieldsParameterError: ...

Source code in src/drf_partial_response_fields/exceptions.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class InvalidFieldsParameterError(PartialResponseFieldsError):
    """Raised when the ``fields`` query parameter cannot be parsed.

    This covers syntax errors such as unbalanced parentheses, empty field
    names, invalid characters, ambiguous combinations of inclusion and
    exclusion within the same group, and expressions that exceed the
    configured maximum nesting depth.

    Args:
        message: A human-readable description of the syntax error,
            including the offending fragment where possible.

    Example:
        >>> from drf_partial_response_fields.parser import parse_fields
        >>> parse_fields("author(name")
        Traceback (most recent call last):
            ...
        drf_partial_response_fields.exceptions.InvalidFieldsParameterError: ...
    """

    default_code = "invalid_fields_parameter"

    def __init__(self, message: str) -> None:
        super().__init__({"fields": [message]}, code=self.default_code)

UnknownFieldError

Bases: PartialResponseFieldsError

Raised when a requested field does not exist on the serializer.

Only raised when the :ref:STRICT <settings-strict> setting is enabled. When STRICT is disabled (the default), unknown field names are silently ignored so that clients cannot probe for the existence of hidden fields.

Parameters:

Name Type Description Default
message str

A human-readable description naming the unknown field(s).

required
Source code in src/drf_partial_response_fields/exceptions.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
class UnknownFieldError(PartialResponseFieldsError):
    """Raised when a requested field does not exist on the serializer.

    Only raised when the
    :ref:`STRICT <settings-strict>` setting is enabled. When ``STRICT`` is
    disabled (the default), unknown field names are silently ignored so
    that clients cannot probe for the existence of hidden fields.

    Args:
        message: A human-readable description naming the unknown field(s).
    """

    default_code = "unknown_field"

    def __init__(self, message: str) -> None:
        super().__init__({"fields": [message]}, code=self.default_code)

Settings

get_setting

Return the effective value of a single package setting.

Parameters:

Name Type Description Default
key str

One of the keys documented in :data:DEFAULTS (e.g. "QUERY_PARAM", "STRICT", "MAX_DEPTH").

required

Returns:

Type Description
Any

The user-configured value if present in the

Any

PARTIAL_RESPONSE_FIELDS Django setting, otherwise the default.

Raises:

Type Description
KeyError

If key is not a recognized setting name.

ImproperlyConfigured

If the PARTIAL_RESPONSE_FIELDS setting is malformed.

Source code in src/drf_partial_response_fields/settings.py
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
def get_setting(key: str) -> Any:
    """Return the effective value of a single package setting.

    Args:
        key: One of the keys documented in :data:`DEFAULTS`
            (e.g. ``"QUERY_PARAM"``, ``"STRICT"``, ``"MAX_DEPTH"``).

    Returns:
        The user-configured value if present in the
        ``PARTIAL_RESPONSE_FIELDS`` Django setting, otherwise the default.

    Raises:
        KeyError: If ``key`` is not a recognized setting name.
        django.core.exceptions.ImproperlyConfigured: If the
            ``PARTIAL_RESPONSE_FIELDS`` setting is malformed.
    """
    return app_settings[key]

See Settings for the full list of recognized keys and their defaults.

OpenAPI (drf-spectacular)

Requires the openapi extra: pip install drf-partial-response-fields[openapi].

fields_query_parameter

Build an :class:~drf_spectacular.utils.OpenApiParameter for fields.

Parameters:

Name Type Description Default
description str | None

Custom description text. Defaults to a general explanation of the sparse-fieldset syntax.

None

Returns:

Type Description
OpenApiParameter

An OpenApiParameter describing the configured QUERY_PARAM

OpenApiParameter

setting, suitable for passing to @extend_schema(parameters=[...]).

Example

.. code-block:: python

from drf_spectacular.utils import extend_schema
from drf_partial_response_fields.openapi import fields_query_parameter


@extend_schema(parameters=[fields_query_parameter()])
class ArticleViewSet(PartialResponseMixin, viewsets.ModelViewSet):
    ...
Source code in src/drf_partial_response_fields/openapi.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
def fields_query_parameter(*, description: str | None = None) -> OpenApiParameter:
    """Build an :class:`~drf_spectacular.utils.OpenApiParameter` for ``fields``.

    Args:
        description: Custom description text. Defaults to a general
            explanation of the sparse-fieldset syntax.

    Returns:
        An ``OpenApiParameter`` describing the configured ``QUERY_PARAM``
        setting, suitable for passing to ``@extend_schema(parameters=[...])``.

    Example:
        .. code-block:: python

            from drf_spectacular.utils import extend_schema
            from drf_partial_response_fields.openapi import fields_query_parameter


            @extend_schema(parameters=[fields_query_parameter()])
            class ArticleViewSet(PartialResponseMixin, viewsets.ModelViewSet):
                ...
    """
    param_name = get_setting("QUERY_PARAM")
    return OpenApiParameter(
        name=param_name,
        type=str,
        location=OpenApiParameter.QUERY,
        required=False,
        description=description
        or (
            "Comma-separated list of fields to include in the response, "
            "e.g. 'id,name'. Supports nested selection with parentheses "
            "(e.g. 'author(name,email)'), exclusion with a '-' prefix "
            "(e.g. '-internal_notes'), and renaming with 'alias:field'. "
            "Omit to receive the full default representation."
        ),
    )

PartialResponseAutoSchema

Bases: AutoSchema

A drf-spectacular AutoSchema that documents ?fields= automatically.

Assign this to a view's schema attribute (or set it as the project default via SPECTACULAR_SETTINGS["DEFAULT_GENERATOR_CLASS"] / per-view schema =) to have the fields query parameter appear in the generated OpenAPI schema for every action, without repeating @extend_schema on each view.

Source code in src/drf_partial_response_fields/openapi.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
class PartialResponseAutoSchema(_SpectacularAutoSchema):
    """A drf-spectacular ``AutoSchema`` that documents ``?fields=`` automatically.

    Assign this to a view's ``schema`` attribute (or set it as the project
    default via ``SPECTACULAR_SETTINGS["DEFAULT_GENERATOR_CLASS"]`` /
    per-view ``schema =``) to have the ``fields`` query parameter appear in
    the generated OpenAPI schema for every action, without repeating
    ``@extend_schema`` on each view.
    """

    def get_override_parameters(self) -> list[Any]:
        """Return drf-spectacular's normal override parameters plus ``fields``.

        Returns:
            The list of parameters from the parent implementation with a
            :func:`fields_query_parameter` appended.
        """
        params = list(super().get_override_parameters())
        params.append(fields_query_parameter())
        return params

get_override_parameters()

Return drf-spectacular's normal override parameters plus fields.

Returns:

Type Description
list[Any]

The list of parameters from the parent implementation with a

list[Any]

func:fields_query_parameter appended.

Source code in src/drf_partial_response_fields/openapi.py
77
78
79
80
81
82
83
84
85
86
def get_override_parameters(self) -> list[Any]:
    """Return drf-spectacular's normal override parameters plus ``fields``.

    Returns:
        The list of parameters from the parent implementation with a
        :func:`fields_query_parameter` appended.
    """
    params = list(super().get_override_parameters())
    params.append(fields_query_parameter())
    return params