skip to content

In Django REST Framework, why should a view using OrderingFilter declare ordering_fields explicitly, and what happens if you leave it unset or use '__all__'?

level: middleimportance: must knowfreq 50%

answer

  1. a whitelist for ?ordering=
  2. unset falls back to the serializer
  3. __all__ means every model column
  4. unknown terms dropped silently
  5. sorting leaks hidden values

basics

~10 s

OrderingFilter's ordering_fields is the whitelist for ?ordering=. Unset, it allows every readable serializer field; 'all' allows every concrete model field and annotation. An explicit list stops clients sorting by hidden or unindexed columns.

solid answer

~40 s

`OrderingFilter` lets the client pick `order_by()` through `?ordering=`, comma-separated, with `-` for descending. `ordering_fields` decides which names are accepted. Left unset, it defaults to the readable fields of the view's serializer (write-only fields, `source='*'` and model properties excluded), and raises `ImproperlyConfigured` if the view has no serializer. `'__all__'` allows every concrete model field plus queryset annotations, including columns you never serialize. Terms outside the list are silently dropped; if none survive, the view's `ordering` attribute applies. Being explicit matters because sorting by a hidden column such as a password hash or an internal score lets a client infer its values, because the orderable surface otherwise grows whenever someone edits the serializer, and because every allowed field is a sort the database must serve efficiently.

code

python · 15 lines
python
from rest_framework import filters, generics

from jobs.models import Job
from jobs.serializers import JobSerializer


class JobListView(generics.ListAPIView):
    queryset = Job.objects.select_related("company")
    serializer_class = JobSerializer
    filter_backends = [filters.OrderingFilter]
    ordering_fields = ["posted_at", "salary_max", "company__name"]
    ordering = ["-posted_at"]

# ?ordering=-salary_max,internal_score -> order_by('-salary_max')
# ?ordering=internal_score            -> falls back to order_by('-posted_at')

go deeper

for a junior

Know the ?ordering= syntax with commas and a leading minus, and that ordering_fields lists what clients may sort by.

for a middle

Explain the three modes of ordering_fields, the silent dropping of invalid terms, and the fallback to the view's ordering attribute.

for a senior

Argue for an explicit list on security and performance grounds: sort-based inference of hidden values, and sorts the database must be indexed to serve.

for a principal

Treat the orderable fields as a contract: every one is an index and a compatibility promise, so additions go through the same review as any new API field.

## What OrderingFilter does `rest_framework.filters.OrderingFilter` is DRF's backend for **client-chosen sorting**. It reads the `ordering` query parameter (renamable with the `ORDERING_PARAM` setting), splits it on commas, and calls `queryset.order_by(*terms)`. A leading `-` means descending, so `?ordering=-salary_max,posted_at` sorts by maximum salary descending, then by posting date. The call to `order_by()` **replaces** any ordering the queryset already carried. Which names a client may use is controlled by `ordering_fields`, a class attribute on the view. ## Three ways to set ordering_fields | Setting | Names the client may sort by | Risk | |---|---|---| | unset (`None`) | readable fields of the serializer from `get_serializer_class()` | surface changes whenever the serializer changes; `ImproperlyConfigured` if there is no serializer | | an explicit list, e.g. `['posted_at', 'salary_max']` | exactly those names, which may include related paths like `company__name` | the safe default | | `'__all__'` | every concrete field in `model._meta.fields` plus every annotation on the queryset | exposes columns the API never shows | Details of the unset case: - write-only serializer fields are excluded, and so are fields with `source='*'`; - fields backed by a Python `@property` on the model are excluded, because the database cannot sort by them; - a field with a dotted source such as `source='company.name'` becomes orderable under the path `company__name`, not under the serializer field's own name. `'__all__'` covers the model's forward concrete fields (foreign keys included, many-to-many excluded) and annotations; it does not open arbitrary `__` traversal into related models. ## How a request is resolved `OrderingFilter.get_ordering()` works in this order: 1. read `?ordering=` and split it on commas; 2. strip a leading `-` from each term and keep the term only if its name is in the valid list; 3. if at least one term survives, order by the survivors; 4. otherwise use the view's `ordering` attribute (a string or a list); 5. if that is unset too, return the queryset unchanged, keeping whatever `get_queryset()` or the model's `Meta.ordering` produced. Invalid terms never produce an error. A client sending `?ordering=password` to a view that does not allow it simply gets the default order, which makes typos easy to miss in testing. ## Why the list should be explicit - **Data leakage through sorting.** A field that is never serialized can still be revealed by sorting on it. If `ordering=salary_min` works on a job board that hides minimum salaries, a client can create or observe listings with known values and see where the hidden ones fall, narrowing each value by bisection. The DRF docs cite ordering against a password hash as the classic case. - **Surface creep.** With the serializer default, adding a field to the serializer silently makes it sortable. An explicit list is a reviewed, versioned part of the API contract. - **Performance.** Every allowed name is a sort the database must perform on demand. On a large table an unindexed sort column turns into a full sort per request, so the list should contain columns you are prepared to index. - **Predictable pagination.** Knowing the allowed fields lets you guarantee a unique tiebreaker, so paged results do not repeat or skip rows. ## Setting a default The view's `ordering` attribute is the fallback, for example `ordering = ['-posted_at']`. The DRF docs note you could instead put `order_by()` on the base queryset; the attribute's advantage is that the browsable API knows the current ordering. Either way, keep a deterministic default, because an unordered queryset gives the paginator an unstable sequence.

  • What does OrderingFilter do when every requested term is invalid?
    `remove_invalid_fields()` returns an empty list, so `get_ordering()` falls back to the view's `ordering` attribute. If that is unset as well, the filter returns the queryset untouched, keeping the `order_by()` from `get_queryset()` or the model's `Meta.ordering`. No 400 is raised, so clients are not told their parameter was ignored.
  • How can sorting by a field that is never serialized leak its value?
    Ordering changes where each row appears relative to rows the client can see or create. By inserting or watching records with known values and checking the position of a target row, a client bisects the hidden value. Allowing only fields that are already public removes that oracle.

saying these in an interview costs you the question

  • Without ordering_fields, OrderingFilter ignores the ?ordering= parameter
  • A disallowed ordering term makes DRF return 400 Bad Request
  • Sorting cannot leak a field that the serializer never outputs
  • ordering_fields = '__all__' only exposes the serializer's fields
  • The view's ordering attribute is appended to the client's ordering