In Django REST Framework, how do request.data and request.query_params differ from Django's request.POST and request.GET?
answer
- body versus URL
- parsed by content type
- any method, not only POST
- errors surface on first access
basics
~10 srequest.query_params is DRF's name for Django's request.GET. request.data is the body parsed by the view's parsers from its Content-Type, for POST, PUT and PATCH alike, JSON included; Django's request.POST holds only form-encoded POST data.
solid answer
~40 s`request.query_params` is simply `request._request.GET` under a clearer name: the URL's query string, available for any method. `request.data` is the request body, parsed lazily on first access by whichever of the view's parsers matches the `Content-Type` — JSON, form-encoded and multipart by default — for POST, PUT and PATCH alike. JSON gives a `dict` (or list), forms give a `QueryDict`, and multipart data includes the uploaded files. Django's `request.POST`, by contrast, is filled only for POST requests with form content types, so a JSON body leaves it empty. Parsing errors surface when `request.data` is first read: malformed JSON raises `ParseError` (400) and an unsupported content type `UnsupportedMediaType` (415). Everything else, such as `request.META`, is proxied to the wrapped `HttpRequest`.
code
python · 20 linesfrom decimal import Decimal
from rest_framework.decorators import api_view
from rest_framework.response import Response
from fx.services import rate_for
@api_view(["POST"])
def convert_batch(request):
target = request.query_params.get("to", "USD") # from the URL
items = request.data # parsed JSON body
if not isinstance(items, list):
return Response({"detail": "expected a JSON list"}, status=400)
results = [
{"from": i["from"], "amount": i["amount"],
"result": str(Decimal(i["amount"]) * rate_for(i["from"], target))}
for i in items
]
return Response(results)go deeper
Remember: query_params is the URL query string, data is the parsed body for POST, PUT and PATCH, and Django's request.POST stays empty for JSON.
Explain parser selection by Content-Type, lazy parsing on first access, the 400 and 415 cases, and why data can be a dict, a list or a QueryDict.
Never trust request.data's shape: validate through serializers, restrict parser_classes per view where uploads or forms are not expected, and handle list bodies deliberately.
Decide which content types an API accepts at all; narrowing default parsers reduces attack surface and keeps client contracts explicit.
## Two places a client puts input An HTTP request carries input in two places: the **query string** in the URL (`/convert/?from=EUR&to=USD&amount=100`) and the **body** (a JSON document, a form, a file upload). Django's `HttpRequest` exposes the query string as `request.GET` and form bodies as `request.POST`. Django REST Framework (DRF) wraps that object in its own `Request` and adds two properties designed for APIs. ## `request.query_params` - It returns the wrapped request's `GET` `QueryDict` — the DRF source describes it as a more semantically correct name for `request.GET`. - It is available for **every** method: a `DELETE` or `POST` can carry query parameters too. - Values are strings; a repeated key (`?tag=a&tag=b`) needs `getlist()`. ## `request.data` - It is the **parsed body**, produced by the view's `parser_classes` (default `JSONParser`, `FormParser`, `MultiPartParser`). - The parser is chosen by matching the request's `Content-Type` against each parser's media type. - It works for **POST, PUT and PATCH** alike. - It is **lazy**: nothing is parsed until the first access, and the result is cached. - For multipart requests it merges form fields and uploaded files; `request.FILES` holds the files alone. ## How it differs from Django's `request.POST` | Input | Django `request.POST` | DRF `request.data` | |---|---|---| | POST, `application/x-www-form-urlencoded` | form fields | same fields, as a `QueryDict` | | POST, `application/json` | empty | parsed `dict` or list | | PUT or PATCH, any body | empty (Django fills `POST` only for POST) | parsed body | | multipart with a file | fields; files in `request.FILES` | fields **and** files | | no body | empty | `{}` (an empty `QueryDict` for form content types) | DRF's own `request.POST` exists for compatibility: it returns the parsed data only for form content types and an empty `QueryDict` otherwise. ## Errors appear on first access Because parsing is lazy, a bad body fails where your code first reads `request.data`, raised as an API exception that the view turns into a response: 1. **Malformed JSON** raises `ParseError` — status 400, with a detail such as `JSON parse error - ...`. 2. **A content type no parser accepts** (say `text/csv` with default parsers) raises `UnsupportedMediaType` — status 415. 3. After a failure DRF stores empty data, so later access does not re-raise the same error while rendering the error response. ## Working with the returned types - A JSON body can be a **list** as well as an object; code that calls `request.data.get(...)` breaks on a list. Validate with a serializer instead of trusting the shape. - A form `QueryDict` is **immutable**; call `.copy()` before modifying it. - In the currency service, a `GET /convert/` reads `query_params`, while a batch `POST /convert/batch/` with a JSON list of amounts reads `data`. ## Parsing and the raw body DRF reads the body in two ways, and it matters if other code also wants the raw bytes: - For `JSONParser` and `FormParser`, DRF parses a copy of `request.body`, which Django caches, so `request.body` stays readable afterwards — useful for verifying a webhook signature. - `MultiPartParser` reads the request stream directly. Reading `request.body` after `request.data` on a multipart request then raises Django's `RawPostDataException` (*You cannot access body after reading from request's data stream*). - If middleware already read `request.POST` on a multipart POST, DRF detects the exhausted stream and reuses Django's parsed `POST` and `FILES`. ## Everything else is proxied Attributes DRF does not define — `request.META`, `request.headers`, `request.session`, `request.path`, `request.method` — are forwarded to the wrapped `HttpRequest` through `__getattr__`. The original is also available as `request._request`, which is rarely needed.
- Why does request.data sometimes behave like a dict and sometimes like a QueryDict?Its type comes from the parser. `JSONParser` returns whatever the JSON holds, usually a `dict` or a list; `FormParser` and `MultiPartParser` return a `QueryDict`, which keeps repeated keys and is immutable. Code that must handle both should go through a serializer rather than index `request.data` directly.
- When is a malformed JSON body detected, and what status does the client get?Only when something first reads `request.data`, for example `serializer.is_valid()` or your own code. `JSONParser` then raises `ParseError`, which the view turns into a 400 response. If nothing reads the body, the malformed JSON is never noticed.
saying these in an interview costs you the question
- request.query_params only works for GET requests.
- request.POST holds the parsed JSON body in a DRF view.
- request.data is parsed eagerly before the handler runs.
- PUT and PATCH bodies must be read from request.body by hand in DRF.
- request.data is always a dict.