skip to content

In Django REST Framework's APIClient, what does format='json' change, and what is sent without it?

level: middleimportance: should knowfreq 55%

answer

  1. a test setting picks the encoding
  2. the default is form-shaped
  3. nested data and None break
  4. request body only, not the response

basics

~10 s

format='json' makes APIClient render the body with JSONRenderer and send Content-Type application/json. Without it, DRF uses TEST_REQUEST_DEFAULT_FORMAT, which defaults to 'multipart', so nested dicts and None cannot be sent.

solid answer

~30 s

`format` picks the renderer `APIClient` uses to encode the request body. With `format='json'` the payload goes through `JSONRenderer` and the request carries `Content-Type: application/json`, which is what a real API client sends. Without it DRF falls back to `TEST_REQUEST_DEFAULT_FORMAT`, whose default is `'multipart'`: nested dicts fail an assertion, `None` raises a `TypeError`, booleans become strings and an omitted `BooleanField` is read as `False`. So a multipart test can pass while exercising form-parsing rules no JSON client hits. Most API projects set `TEST_REQUEST_DEFAULT_FORMAT` to `'json'` and keep `format='multipart'` only for file uploads. `format` affects only the request body, never the response format.

code

python · 25 lines
python
from django.contrib.auth import get_user_model
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase


class ClaimPayloadEncodingTests(APITestCase):
    def setUp(self):
        user = get_user_model().objects.create_user(username="dana")
        self.client.force_authenticate(user=user)

    def test_nested_payload_needs_json(self):
        payload = {"amount": "10.00", "receipt": {"vendor": "Rail", "number": None}}

        with self.assertRaises(AssertionError):
            self.client.post(reverse("claim-list"), payload)  # multipart default

        response = self.client.post(reverse("claim-list"), payload, format="json")
        self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data)

    def test_malformed_json_is_rejected(self):
        response = self.client.post(
            reverse("claim-list"), data="{not json", content_type="application/json"
        )
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

go deeper

for a junior

Remember that APIClient's default encoding is multipart and that format='json' is what makes a test body look like a real API client's request.

for a middle

Explain the two settings, TEST_REQUEST_DEFAULT_FORMAT and TEST_REQUEST_RENDERER_CLASSES, and which payloads break or silently change under multipart encoding.

for a senior

Point out the green-test trap: form parsing reads an omitted BooleanField as False and sends lists as repeated keys, so a multipart test can pass where every JSON client fails.

for a principal

Decide the suite-wide default once in settings, and state when content_type with raw bytes is required to test parser and error paths deliberately.

## Two settings decide how a test body is encoded `APIRequestFactory` (and `APIClient`, which inherits from it) encodes the `data` you pass to `post()`, `put()`, `patch()`, `delete()` and `options()` with a **renderer**, chosen by the `format` argument. Two settings in `REST_FRAMEWORK` control this: - **`TEST_REQUEST_DEFAULT_FORMAT`** — the format used when you pass neither `format` nor `content_type`. Its default is **`'multipart'`**. - **`TEST_REQUEST_RENDERER_CLASSES`** — the renderers available to tests. The default is `MultiPartRenderer` and `JSONRenderer`, so the two legal values of `format` out of the box are `'multipart'` and `'json'`. So `self.client.post(url, payload)` sends `multipart/form-data`, the same shape as an HTML form upload. `self.client.post(url, payload, format="json")` renders the payload with `JSONRenderer` and sets `Content-Type: application/json`. ## Why the multipart default bites API tests Multipart form data is a flat list of string key/value pairs. A real API client almost always sends JSON, and several things that JSON carries cannot be expressed in a form: | Payload | With `format="json"` | With the multipart default | |---|---|---| | `{"address": {"city": "Oslo"}}` | Sent as a nested object | `AssertionError`: multipart does not support nested data (the message suggests `format='json'`) | | `{"note": None}` | Sent as `null` | `TypeError` from Django's multipart encoder: cannot encode `None` | | `{"urgent": True}` | Sent as `true` | Sent as the string `"True"`; DRF's `BooleanField` lowercases and accepts it | | a `BooleanField` left out | Missing; a required field fails validation | Read as `False` on a non-partial request, because form input cannot express "absent" for a checkbox | | `{"tags": ["a", "b"]}` | Sent as a JSON array | Sent as repeated `tags` keys | The last two rows are the dangerous ones: the test **passes**, but it exercised form-parsing rules (`BooleanField`'s `default_empty_html = False`, the `getlist()` path for lists) that a JSON client never hits. A validation test that forgot a required boolean can go green under multipart and red for every real client. ## What format does not change - **The response format.** `format=` only encodes the *request* body. Which renderer produces the response is decided by content negotiation on the view (the `Accept` header, the renderer classes); `response.data` is the same Python data either way. - **GET requests.** `APIRequestFactory.get()` takes no `format` argument: its `data` becomes the query string. - **The view's parsers.** The view must still accept the content type. DRF's default `DEFAULT_PARSER_CLASSES` are `JSONParser`, `FormParser` and `MultiPartParser`, so both test formats parse; a view restricted to `JSONParser` will answer a multipart test request with `415 Unsupported Media Type`. ## Reading the symptoms of the wrong encoding - **An exception before any request is sent** (`AssertionError` about nested data, `TypeError` about `None`) — the payload cannot be expressed as multipart; add `format="json"`. - **`415 Unsupported Media Type`** — the view's `parser_classes` do not accept the encoding the test used. - **A `400` on a field you clearly sent, usually a list, nested or boolean field** — form parsing read it differently from how a JSON parser would; resend it as JSON before debugging the serializer. - **A test that passes only without `format="json"`** — suspect a field that depends on form semantics, such as a `BooleanField` that silently defaults to `False` when omitted. ## `format` versus `content_type` The two arguments are mutually exclusive — passing both fails an assertion ("You may not set both `format` and `content_type`."). They do different jobs: 1. **`format="json"`** — you pass Python data; a renderer turns it into bytes and supplies the header. 2. **`content_type="application/json"`** — you pass an already-encoded body. DRF first tries Django's own JSON encoding for JSON content types, then sends the bytes as they are. This is the tool for sending a deliberately malformed body, e.g. `data="{not json", content_type="application/json"` to assert a `400` from the parser. 3. **An unknown `format`** — such as `format="xml"` without an XML renderer in `TEST_REQUEST_RENDERER_CLASSES` — fails an assertion that lists the available formats and tells you to add a renderer. ## Setting the project default Most API-only projects flip the default once instead of repeating `format="json"` in every test: ```python REST_FRAMEWORK = { "TEST_REQUEST_DEFAULT_FORMAT": "json", } ``` Keep an explicit `format="multipart"` in the tests that upload files, since a file object only travels in multipart. Mixing the two in one suite is normal: JSON for the resource endpoints, multipart for the upload endpoint.

  • How do you test a malformed JSON body if format='json' always produces valid JSON?
    Pass the raw text with `content_type='application/json'` instead of `format`, for example `data='{not json'`. The test client sends the bytes unchanged, `JSONParser` fails, and the view answers `400 Bad Request` with a parse-error detail. `format` and `content_type` cannot be combined in one call.
  • Should the project set TEST_REQUEST_DEFAULT_FORMAT to 'json'?
    For an API whose clients send JSON, yes: tests then match production by default and nobody forgets `format='json'`. File-upload tests pass `format='multipart'` explicitly, because a file object is only encoded in multipart. The setting changes tests only; it has no effect on how the running API parses requests.

saying these in an interview costs you the question

  • Believing APIClient sends JSON by default because DRF is a JSON API framework
  • Thinking format='json' also forces the response to be rendered as JSON
  • Passing format='json' to a GET and expecting a JSON request body
  • Combining format='json' with content_type='application/json' in the same call
  • Trusting a green multipart test to prove a JSON client's payload validates