skip to content

Reply Classes & Streaming

HttpResponse and its subclasses, from JsonResponse and redirects to StreamingHttpResponse, FileResponse and TemplateResponse, each with its own body rules. Interviewers ask when to stream.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

6

In Django, why does JsonResponse raise a TypeError when you pass it a list, and what does safe=False change?

level: middleimportance: must knowfreq 58%

answer

  1. a guard on the top-level type
  2. only dict passes by default
  3. old Array-constructor attack
  4. DjangoJSONEncoder handles dates and Decimals

basics

~10 s

JsonResponse accepts only a dict by default (safe=True) because of an old JSON-array hijacking attack. Passing safe=False allows a list or any other JSON-serializable value as the top-level body.

solid answer

~40 s

`JsonResponse` is an `HttpResponse` subclass that runs `json.dumps()` with `DjangoJSONEncoder` and sets `Content-Type: application/json`. Its `safe` argument defaults to `True`, and in that mode the constructor raises `TypeError` unless `data` is a `dict`. The guard dates from browsers before ECMAScript 5, where a top-level JSON array could be read cross-site by poisoning the `Array` constructor. Modern browsers closed that hole, so `safe=False` is fine for a list, but a dict envelope such as `{"results": [...]}` is still the better API shape because you can add keys later. `safe` only relaxes the type check: a `QuerySet` or model instance still fails to serialize, so convert with `list(qs.values(...))` first.

code

python · 17 lines
python
from django.http import JsonResponse

from .models import StockItem


def low_stock(request):
    rows = list(
        StockItem.objects.filter(qty__lt=10).values("sku", "qty", "updated_at")
    )
    # Preferred: a dict envelope, safe=True stays on
    return JsonResponse({"count": len(rows), "results": rows})


def low_stock_bare(request):
    rows = list(StockItem.objects.filter(qty__lt=10).values_list("sku", flat=True))
    # A top-level list needs safe=False, otherwise TypeError is raised
    return JsonResponse(rows, safe=False)

go deeper

for a junior

Remember that JsonResponse takes a dict by default and raises TypeError for a list unless you pass safe=False.

for a middle

Explain that safe is a top-level type gate, separate from serialization, and list what DjangoJSONEncoder can and cannot encode.

for a senior

Argue for a dict envelope as an API design choice and show how you convert querysets deliberately rather than leaking model fields.

for a principal

Frame safe=True as a legacy browser defence turned API-evolution convention, and decide when a team standard should forbid bare arrays.

## What JsonResponse is `django.http.JsonResponse` is a thin subclass of `HttpResponse`. Its constructor signature is `JsonResponse(data, encoder=DjangoJSONEncoder, safe=True, json_dumps_params=None, **kwargs)`. It does three things and nothing more: 1. checks the type of `data` when `safe` is `True`; 2. serializes `data` with `json.dumps(data, cls=encoder, **json_dumps_params)`; 3. passes the resulting string to `HttpResponse` as the body, with `content_type` defaulting to `application/json`. Every other keyword (`status`, `headers`, `content_type`) goes straight to `HttpResponse`, so `JsonResponse({"id": 7}, status=201)` is the usual way to answer a successful create. ## The safe flag and where it comes from With the default `safe=True`, the constructor raises `TypeError` with the message *"In order to allow non-dict objects to be serialized set the safe parameter to False."* whenever `data` is not an instance of `dict`. That means a `list`, a `tuple`, a string, a number or `None` all fail, while a `dict` subclass passes. The reason is historical. Before the 5th edition of ECMAScript, a page on another origin could include your JSON URL as a `<script>` and, by redefining the `Array` constructor, read the values of a top-level JSON array. A top-level object literal is not a valid script statement, so it was not exposed the same way. Django chose to make the risky shape opt-in. Modern browsers implement ECMAScript 5, which removed that attack vector, and the Django documentation says it is possible to disable the precaution. - `JsonResponse({"items": rows})` works with the default. - `JsonResponse(rows, safe=False)` works when `rows` is a list. - `JsonResponse(rows)` with a list raises `TypeError` before any JSON is produced. ## Why a dict is still the better shape The documentation adds a design argument that survives the security one: an API built on objects is more extensible. If an endpoint returns a bare array, adding pagination metadata, a warning or a version field later is a breaking change. If it returns `{"results": [...]}`, a `next` or `count` key can be added without breaking a single client. Many teams therefore keep `safe=True` on purpose and treat a `TypeError` as a code-review signal. ## What safe=False does not do `safe` is only a type gate on the top-level value. It does not make arbitrary Python objects serializable. `DjangoJSONEncoder` extends the standard encoder with: | Python type | Encoded as | |---|---| | `datetime`, `date`, `time` | ISO 8601 string | | `timedelta` | ISO 8601 duration string | | `Decimal`, `UUID` | string | | lazy translation strings (`Promise`) | string | A model instance, a `QuerySet` or a set is none of these, so `json.dumps` still raises `TypeError` ("is not JSON serializable") even with `safe=False`. The fix is to build plain data first: `list(Item.objects.values("sku", "qty"))`, a list comprehension, or a serializer from an API framework. You can also pass a custom `encoder` subclass of `DjangoJSONEncoder` and override `default()`. ## Other knobs worth knowing - `json_dumps_params` forwards keyword arguments to `json.dumps`, e.g. `{"ensure_ascii": False}` to keep non-ASCII characters readable or `{"indent": 2}` for a debug endpoint. - The body is serialized eagerly in the constructor, so a very large payload is held in memory as one string; that is a reason to stream or paginate, not to reach for `JsonResponse`. - On Django 5.2 and later, `HttpResponse.text` returns the decoded body, which is handy in tests: `json.loads(response.text)` or the test client's `response.json()`. ## JsonResponse versus a hand-built HttpResponse You could write `HttpResponse(json.dumps(data), content_type="application/json")` yourself, and older code often does. `JsonResponse` is preferred because it bundles three decisions in one place: - the encoder is `DjangoJSONEncoder`, so a `Decimal` stock value or a `datetime` timestamp does not crash with the standard library's default encoder; - the content type is set for you, and you can still override it (for example `application/problem+json`) through `content_type`; - the `safe` guard makes a top-level list a conscious choice. Error responses follow the same pattern: `JsonResponse({"error": "unknown sku"}, status=404)` gives the client a machine-readable body with the right status. For a whole JSON API with parsing, validation, content negotiation and pagination, teams usually adopt an API framework rather than hand-rolling `JsonResponse` views, but `JsonResponse` remains the right tool for a handful of AJAX endpoints inside a server-rendered project. ## Interview framing A good answer names the default (`safe=True`), the exception (`TypeError`), the historical reason (the pre-ES5 `Array` hijack), and then the practical conclusion: turn it off for a list if you must, but prefer a dict envelope, and remember that `safe` never teaches the encoder about querysets.

  • With safe=False, what happens if you pass a QuerySet straight to JsonResponse?
    It still fails. `safe=False` only lifts the top-level `dict` check; `json.dumps` then hands the `QuerySet` to `DjangoJSONEncoder.default()`, which knows dates, `Decimal`, `UUID` and lazy strings but not querysets, so it raises `TypeError: Object of type QuerySet is not JSON serializable`. Wrap it in `list(qs.values(...))` or serialize it explicitly.
  • How would you return JSON with a 201 status and non-ASCII product names left readable?
    Pass HttpResponse keyword arguments through: `JsonResponse(data, status=201, json_dumps_params={"ensure_ascii": False})`. `status` is handled by `HttpResponse`, and `json_dumps_params` is forwarded to `json.dumps`, so accented or CJK names are emitted as UTF-8 instead of `\u` escapes.
  • Why do many teams keep safe=True even though browsers fixed the original attack?
    Because a dict envelope is easier to evolve. Returning `{"results": [...]}` lets you add `count`, `next` or a deprecation notice later without breaking clients, while a bare array cannot grow new top-level fields. Keeping the default makes a bare list a deliberate, reviewed choice.

saying these in an interview costs you the question

  • safe=False disables HTML escaping or XSS protection in the output
  • JsonResponse can serialize a QuerySet directly once safe=False is set
  • The TypeError comes from json.dumps failing on lists
  • A top-level JSON array is still exploitable in every modern browser
  • JsonResponse needs content_type='application/json' passed by hand
open as a page

In Django, what does FileResponse do for a file download that a plain HttpResponse or StreamingHttpResponse does not?

level: middleimportance: should knowfreq 38%

basics

~20 s

FileResponse streams a binary file-like object in blocks, closes it afterwards, and sets Content-Length, Content-Type and Content-Disposition from the file where it can. as_attachment=True and filename control whether the browser downloads it and under what name.

open as a page

In Django, how does returning a TemplateResponse differ from returning the HttpResponse built by render(), and why does its deferred rendering matter?

level: middleimportance: should knowfreq 34%

basics

~10 s

render() produces an HttpResponse whose body is already rendered, while TemplateResponse keeps the template and context and renders only after the view returns. Decorators, middleware and tests can change or inspect them before rendering.

open as a page

A Django view that exports warehouse inventory as CSV runs out of memory and times out on large warehouses; how would you stream it with StreamingHttpResponse, and what do you give up?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Return a StreamingHttpResponse over a generator that yields CSV rows built with csv.writer and QuerySet.iterator(), so memory stays flat and bytes flow early. You lose Content-Length, ETags, caching and the chance to change the status mid-stream.

open as a page

In Django 5.2 and later, what does preserve_request=True change on HttpResponseRedirect and HttpResponsePermanentRedirect, and when do you need it?

level: middleimportance: nice to knowfreq 24%

basics

~20 s

preserve_request=True switches HttpResponseRedirect from 302 to 307 and HttpResponsePermanentRedirect from 301 to 308, telling the client to repeat the same method and body. Use it when redirecting a non-GET request, such as a moved POST endpoint.

open as a page