In Django REST Framework, how do you change the paginated response envelope with get_paginated_response(), and how do you paginate inside a plain APIView?
answer
- one method builds the body
- the schema method should match
- three calls outside generic views
- a None page means no pagination
basics
~10 sSubclass a pagination class and override get_paginated_response(data) to return any Response, such as bare results with Link headers, plus get_paginated_response_schema() for OpenAPI. In an APIView, call paginate_queryset(), serialise the page, then call get_paginated_response().
solid answer
~40 sEvery DRF pagination class ends with `get_paginated_response(data)`, which receives the serialised page and returns a `Response`. The built-ins return `{count, next, previous, results}` (cursor: no `count`), but a subclass can return anything — for example the bare list with `Link` and `X-Total-Count` headers built from `self.get_next_link()`, `self.get_previous_link()` and `self.page.paginator.count`. Override `get_paginated_response_schema()` too, or the generated OpenAPI schema describes an envelope you no longer send. Outside generic views nothing is automatic: an `APIView` instantiates the class, calls `paginate_queryset(queryset, request, view=self)`, serialises the returned list with `many=True`, and returns `paginator.get_paginated_response(serializer.data)`. `paginate_queryset()` returns `None` when no page size is configured, so handle that or set `page_size` on the class.
code
python · 19 linesfrom rest_framework.pagination import CursorPagination
from rest_framework.views import APIView
from .models import Activity
from .serializers import ActivitySerializer
class FeedCursorPagination(CursorPagination):
page_size = 30
ordering = ("-created", "-id")
class ActivityFeedView(APIView):
def get(self, request):
queryset = Activity.objects.filter(recipient=request.user)
paginator = FeedCursorPagination()
page = paginator.paginate_queryset(queryset, request, view=self)
serializer = ActivitySerializer(page, many=True)
return paginator.get_paginated_response(serializer.data)go deeper
Know that get_paginated_response() builds the paginated body and that APIView needs manual pagination calls.
Explain the split between paginate_queryset() and get_paginated_response(), the None case, and which state each class keeps for its links.
Change the envelope without breaking generated clients by overriding the schema method, and make the custom style the project default.
Weigh body envelopes against Link headers for the API's clients and caches, and fix one convention for every list endpoint.
## The two halves of a DRF pagination class Every pagination class in Django REST Framework (DRF) implements two methods from `BasePagination`: 1. `paginate_queryset(queryset, request, view=None)` — reads the query string, slices the queryset and returns a **list** of objects for this page, or `None` when pagination is not configured (for example, no page size). 2. `get_paginated_response(data)` — receives the **serialised** page and returns the `Response` sent to the client. Generic views glue them together in `ListModelMixin.list()`. Everything about the **wire format** lives in the second method, so changing the envelope never requires touching slicing logic. ## What the built-ins return | Class | Body keys | |---|---| | `PageNumberPagination` | `count`, `next`, `previous`, `results` | | `LimitOffsetPagination` | `count`, `next`, `previous`, `results` | | `CursorPagination` | `next`, `previous`, `results` | The link helpers `get_next_link()` and `get_previous_link()` are public methods on each class and return absolute URLs or `None`, so an override can reuse them. ## Overriding the envelope Common reasons to override `get_paginated_response()`: - return a **bare list** and move navigation into an RFC 8288 `Link` header; - add fields such as `total_pages` or `page_size`; - rename keys to match an existing client contract. ```python from rest_framework.pagination import PageNumberPagination from rest_framework.response import Response class LinkHeaderPagination(PageNumberPagination): page_size = 50 def get_paginated_response(self, data): links = [] if (url := self.get_next_link()) is not None: links.append(f'<{url}>; rel="next"') if (url := self.get_previous_link()) is not None: links.append(f'<{url}>; rel="prev"') headers = {"X-Total-Count": str(self.page.paginator.count)} if links: headers["Link"] = ", ".join(links) return Response(data, headers=headers) def get_paginated_response_schema(self, schema): return schema ``` `self.page` is set by `paginate_queryset()` in `PageNumberPagination`; `LimitOffsetPagination` keeps `self.count`, `self.limit` and `self.offset` instead, so an override must use the attributes of the class it extends. ## Keep the schema honest `get_paginated_response_schema(schema)` wraps the list schema for DRF's OpenAPI generator. The built-ins describe their own envelope; after changing the body you should override it as well — returning `schema` unchanged for a bare list — or generated clients will expect keys that no longer exist. ## Paginating inside an `APIView` `APIView` has no `pagination_class` handling, so `DEFAULT_PAGINATION_CLASS` does nothing there. The manual pattern is three calls: 1. create the paginator: `paginator = FeedCursorPagination()`; 2. `page = paginator.paginate_queryset(queryset, request, view=self)`; 3. serialise `page` with `many=True` and `return paginator.get_paginated_response(serializer.data)`. Passing `view=self` matters for `CursorPagination`, which looks at the view's filter backends when choosing the ordering. If `page` is `None`, serialise the full queryset and return a plain `Response`, exactly as `ListModelMixin.list()` does. ## Pitfalls - Calling `get_paginated_response()` before `paginate_queryset()` fails, because the link helpers depend on state that method sets. - Serialising the queryset instead of `page` silently returns every row inside a paginated envelope. - A custom envelope used by only some endpoints fragments the contract; set it as `DEFAULT_PAGINATION_CLASS` if it is the house style.
- In DRF, why pass view=self to paginate_queryset() from an APIView?Some classes consult the view. `CursorPagination.get_ordering()` inspects `view.filter_backends` for an ordering filter; without the view it cannot, and relies on its own `ordering` only. Passing `view=self` keeps behaviour identical to a generic view and costs nothing.
- Where would you put a total_pages field in a DRF PageNumberPagination response?Override `get_paginated_response()` and add `self.page.paginator.num_pages` alongside `count`, `next`, `previous` and `results`. Update `get_paginated_response_schema()` to add the integer property so the OpenAPI schema matches the body.
saying these in an interview costs you the question
- Changing the envelope requires overriding paginate_queryset().
- An APIView paginates automatically once DEFAULT_PAGINATION_CLASS is set.
- get_paginated_response() receives the queryset rather than serialised data.
- The OpenAPI schema updates itself when get_paginated_response() changes.
- paginate_queryset() always returns a page, never None.