skip to content

Parsers, Renderers & Versioning

Parsers like JSONParser and MultiPartParser read bodies, renderers like JSONRenderer and BrowsableAPIRenderer write replies, and versioning sets request.version. Interviewers probe negotiation.

part ofDjango REST Frameworkoverview, primer and where to startread it →
on this pageshow

explore

questions

4

How do you add a CSV output format to a Django REST Framework endpoint, and what must a custom renderer handle besides successful list data?

level: middleimportance: should knowfreq 33%

answer

  1. subclass BaseRenderer
  2. media_type and format attributes
  3. render() returns bytes
  4. errors and pages go through it too
  5. keep JSON in the list

basics

~10 s

Subclass BaseRenderer with media_type 'text/csv' and format 'csv', return bytes from render(), and add it to renderer_classes alongside JSONRenderer. It must also cope with paginated envelopes, single objects and error dicts.

solid answer

~40 s

A DRF renderer is a `BaseRenderer` subclass with a `media_type` (matched against `Accept`), a `format` (matched against `?format=` and `.csv` suffixes) and a `render(data, accepted_media_type=None, renderer_context=None)` method that returns bytes; a returned `str` is encoded with the renderer's `charset`. Add it to the view's `renderer_classes` together with `JSONRenderer`, because the view attribute replaces `DEFAULT_RENDERER_CLASSES`. Once negotiated, the renderer receives every response from that request: a paginated dict with `results`, a single object for a detail route, `None` for a 204, and error dicts for 400 or 404. `renderer_context` carries the `response`, so `render()` can check `status_code`. Set a `Content-Disposition` header in the view if clients should download a file.

code

python · 28 lines
python
import csv
import io

from rest_framework import renderers


class JobCSVRenderer(renderers.BaseRenderer):
    media_type = "text/csv"
    format = "csv"

    def render(self, data, accepted_media_type=None, renderer_context=None):
        if data is None:
            return b""
        response = (renderer_context or {}).get("response")
        if response is not None and response.status_code >= 400:
            rows = [{"error": str(data)}]
        elif isinstance(data, dict) and "results" in data:
            rows = data["results"]
        elif isinstance(data, dict):
            rows = [data]
        else:
            rows = list(data)
        buffer = io.StringIO()
        if rows:
            writer = csv.DictWriter(buffer, fieldnames=list(rows[0].keys()))
            writer.writeheader()
            writer.writerows(rows)
        return buffer.getvalue().encode(self.charset)

go deeper

for a junior

Recall that a custom renderer subclasses BaseRenderer and sets media_type and format, and that ?format=csv selects it.

for a middle

Explain the render() contract, charset encoding, renderer_context, and why the view's renderer_classes must still include JSONRenderer.

for a senior

Make the renderer robust to pagination envelopes, 204s and error bodies, and choose streaming exports over a renderer when the data is large.

for a principal

Decide whether export formats belong in the API at all or in a dedicated export job, weighing contract surface against client convenience.

## What a renderer is in DRF A Django REST Framework **renderer** converts the data in a `Response` into the bytes of the HTTP body. The framework's own renderers live in `rest_framework.renderers`; a custom one is a subclass of `BaseRenderer` with three things: - `media_type`: the type it produces, compared with the `Accept` header, here `text/csv`; - `format`: the short name used by the `format` query parameter and URL suffixes, here `csv`; - `render(self, data, accepted_media_type=None, renderer_context=None)`: returns the body. `BaseRenderer.charset` defaults to `'utf-8'`. If `render()` returns a `str`, `Response.rendered_content` encodes it with that charset; for binary output you set `charset = None` and return bytes. ## Wiring it into a v1/v2 jobs API 1. Write `JobCSVRenderer` (below). 2. Add it to the view: `renderer_classes = [JSONRenderer, JobCSVRenderer]`. The view attribute **replaces** `DEFAULT_RENDERER_CLASSES`, so list JSON explicitly or JSON clients start receiving 406 responses. 3. Clients ask for CSV with `Accept: text/csv`, with `?format=csv`, or with a `.csv` suffix when the URLconf uses `format_suffix_patterns()` or a router that adds format suffixes (`DefaultRouter` does by default). 4. Optionally set `Content-Disposition: attachment; filename=jobs.csv` in the view when `request.accepted_renderer.format == 'csv'`. Putting JSON first keeps it the answer for clients that send `*/*`, because renderer order decides ties. ## Everything the renderer will receive Negotiation happens in `APIView.initial()`, before the handler. From then on, **every** `Response` for that request is rendered by the negotiated renderer, including errors. A CSV renderer that only expects a list of rows will crash, or produce garbage, on: | Input | When | Handling | |---|---|---| | list of dicts | unpaginated list | header row plus rows | | dict with `results` | paginated list | unwrap `results`; the page links are lost in CSV | | single dict | retrieve, create | one row | | `None` | 204 No Content | return an empty body | | error dict | 400, 403, 404, 429 | check `renderer_context['response'].status_code` | `renderer_context` is a dict the view supplies with `view`, `request`, `response`, `args` and `kwargs`. When negotiation itself fails (406), DRF renders the error with the **first** renderer in the list, another reason to keep JSON first. ## Pitfalls - **Nested data.** Serializer output can contain nested dicts and lists; flatten them, or give CSV requests a flatter serializer by checking `request.accepted_renderer.format` in `get_serializer_class()`. - **Memory.** `render()` builds the whole body in memory. For very large exports, a streaming view that writes rows with Python's `csv` module into a `StreamingHttpResponse` is a better fit than a renderer. - **Spreadsheet formula injection.** Cells starting with `=`, `+`, `-` or `@` can be interpreted as formulas when the file is opened in a spreadsheet; escape them if the data is user-supplied. - **Parsing is separate.** A renderer only writes responses; accepting CSV uploads needs a parser in `parser_classes`. - Maintained third-party renderers exist, but interviewers usually want to see that you know the `BaseRenderer` contract.

  • Why might JSON clients start getting 406 after you add a CSV renderer to one view?
    Setting `renderer_classes = [JobCSVRenderer]` replaces `DEFAULT_RENDERER_CLASSES` instead of extending it. A client sending `Accept: application/json` then matches nothing and receives 406. List `JSONRenderer` alongside the CSV renderer, first if it should remain the default.
  • How would you give CSV clients a flatter representation than JSON clients?
    Override `get_serializer_class()` and return a flat serializer when `self.request.accepted_renderer.format == 'csv'`. Negotiation runs in `initial()` before the handler, so the accepted renderer is known by the time the serializer is chosen.

saying these in an interview costs you the question

  • Adding a renderer to one view appends it to the global defaults
  • A renderer only ever sees successful list data
  • render() may return a Response object with headers
  • The format attribute is only a label with no effect on selection
  • DRF picks the CSV renderer for error responses only if it is listed first
open as a page

In Django REST Framework, why does one endpoint return the Browsable API to a browser but JSON to curl, and how does DefaultContentNegotiation decide?

level: middleimportance: should knowfreq 45%

basics

~20 s

DRF matches the Accept header against the view's renderers: browsers ask for text/html, which BrowsableAPIRenderer serves, while curl sends /, which the first renderer, JSONRenderer by default, satisfies. q-values are ignored; ties go to renderer order.

open as a page

In Django REST Framework, how do URLPathVersioning, NamespaceVersioning and AcceptHeaderVersioning set request.version, and how do you serve a different serializer for v2?

level: middleimportance: should knowfreq 40%

basics

~10 s

With a versioning class set, DRF fills request.version before the handler: URLPathVersioning from the version URL kwarg, NamespaceVersioning from the URL namespace, AcceptHeaderVersioning from Accept's version parameter. Branch on it in get_serializer_class().

open as a page