skip to content

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