JSON in Django: JsonResponse, DRF Serializers, request.data & REST APIs
Last updated:
Django returns JSON with JsonResponse(data) — passing a Python dict produces a Content-Type: application/json response with no manual json.dumps() required; passing a list requires safe=False since Django's default only allows dict top-level values. In plain Django views, json.loads(request.body) reads JSON request bodies, raising json.JSONDecodeError on malformed input. For REST APIs, Django REST Framework (DRF) improves both sides: request.data automatically parses JSON (and other content types), and Response(serializer.data) serializes Python objects with content negotiation. ModelSerializer generates a complete JSON serializer from a Django model in 5 lines. This guide covers JsonResponse, reading JSON bodies in plain Django views, DRF APIView and request.data, ModelSerializer for model-to-JSON serialization, nested serializers, validation with serializer.is_valid(), and returning standard error responses.
JsonResponse: Returning JSON from Django Views
JsonResponse from django.http is the simplest way to return JSON from a Django view. It subclasses HttpResponse, automatically sets the Content-Type header to application/json, and calls json.dumps() on the data you pass. You never need to manually serialize — pass a Python dict and the response is ready. The default status code is 200; pass status= to return any HTTP status code.
from django.http import JsonResponse
from django.views import View
# ── Function-based view ────────────────────────────────────────
def product_list(request):
products = [
{"id": 1, "name": "Widget", "price": 9.99},
{"id": 2, "name": "Gadget", "price": 24.99},
]
# safe=False required for lists as the top-level value
return JsonResponse(products, safe=False)
# ── Dict response — default safe=True ─────────────────────────
def api_status(request):
return JsonResponse({
"status": "ok",
"version": "1.0",
"count": 42,
})
# ── Custom status code ─────────────────────────────────────────
def create_item(request):
# ... save logic ...
return JsonResponse({"id": 99, "created": True}, status=201)
# ── JsonResponse with custom encoder (datetime, Decimal) ───────
import json
from decimal import Decimal
from datetime import datetime
class ExtendedEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime):
return obj.isoformat()
if isinstance(obj, Decimal):
return float(obj)
return super().default(obj)
def order_detail(request, pk):
return JsonResponse(
{
"id": pk,
"total": Decimal("149.99"),
"created_at": datetime(2026, 5, 28, 10, 0, 0),
},
encoder=ExtendedEncoder,
)
# ── Class-based view ───────────────────────────────────────────
class UserView(View):
def get(self, request):
return JsonResponse({"name": "Alice", "email": "alice@example.com"})
def post(self, request):
import json
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({"error": "Invalid JSON"}, status=400)
# ... process data ...
return JsonResponse({"created": True}, status=201)
# ── urls.py ────────────────────────────────────────────────────
# urlpatterns = [
# path("api/products/", product_list),
# path("api/status/", api_status),
# path("api/users/", UserView.as_view()),
# ]JsonResponse passes keyword arguments through to HttpResponse: use content_type= to override the MIME type (rarely needed), status= for the HTTP status code, and headers= (Django 3.2+) to add custom response headers. For CORS headers on JSON APIs, use the django-cors-headers middleware rather than setting headers manually on each response. The json_dumps_params argument (a dict) is forwarded to json.dumps() — use json_dumps_params={"ensure_ascii": False} to preserve Unicode characters without escaping them to \uXXXX sequences.
Reading JSON Request Bodies with json.loads(request.body)
In plain Django views (without DRF), read the request body with request.body — a bytes object containing the raw request body — and decode it with json.loads(). Always wrap in a try/except json.JSONDecodeError block since malformed JSON raises an exception. Do not access request.POST and request.body in the same view: Django reads the stream for request.POST (form data), making request.body empty afterward.
import json
from django.http import JsonResponse, HttpResponseBadRequest
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
# ── Basic JSON body reading ────────────────────────────────────
@csrf_exempt # APIs typically use token auth, not CSRF cookies
@require_http_methods(["POST"])
def create_user(request):
try:
data = json.loads(request.body)
except json.JSONDecodeError as e:
return JsonResponse(
{"error": "Invalid JSON", "detail": str(e)},
status=400,
)
# Validate required fields manually
errors = {}
if not data.get("name"):
errors["name"] = ["This field is required."]
if not data.get("email"):
errors["email"] = ["This field is required."]
if errors:
return JsonResponse({"errors": errors}, status=400)
# data is a plain Python dict — access fields normally
name = data["name"]
email = data["email"]
age = data.get("age") # optional field
# ... save to database ...
return JsonResponse(
{"id": 1, "name": name, "email": email},
status=201,
)
# ── Read body and validate Content-Type ───────────────────────
@csrf_exempt
def api_endpoint(request):
content_type = request.content_type or ""
if "application/json" not in content_type:
return JsonResponse(
{"error": "Content-Type must be application/json"},
status=415,
)
try:
payload = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({"error": "Malformed JSON body"}, status=400)
return JsonResponse({"received": payload})
# ── GET with query params — no body reading needed ─────────────
def search_view(request):
query = request.GET.get("q", "")
page = int(request.GET.get("page", "1"))
limit = min(int(request.GET.get("limit", "20")), 100)
# ... database query ...
results = []
return JsonResponse({
"query": query,
"page": page,
"limit": limit,
"results": results,
"total": 0,
})
# ── PUT/PATCH — same pattern as POST for body reading ─────────
@csrf_exempt
@require_http_methods(["PUT", "PATCH"])
def update_user(request, pk):
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({"error": "Invalid JSON"}, status=400)
# For PATCH: only update provided fields
if request.method == "PATCH":
allowed_fields = {"name", "email", "bio"}
data = {k: v for k, v in data.items() if k in allowed_fields}
# ... update user pk with data ...
return JsonResponse({"id": pk, "updated": True})Note that request.body is re-readable in Django — unlike some frameworks, accessing it multiple times in a single view is safe. The @csrf_exempt decorator is commonly used for API views that authenticate via tokens (Authorization header) rather than CSRF cookies. In production, apply authentication at the middleware level rather than decorating individual views. For method dispatch, use @require_http_methods(["POST", "GET"]) or a class-based view rather than checking request.method manually.
Django REST Framework: request.data and Response
DRF's request.data is a parsed, dictionary-like object that replaces both request.POST (form data) and json.loads(request.body) (JSON). DRF inspects the Content-Type request header and selects the appropriate parser — JSONParser for application/json, FormParser for application/x-www-form-urlencoded, MultiPartParser for file uploads. On the response side, Response(data) inspects the Accept header and selects the renderer — JSON for programmatic clients, browsable HTML for browser requests.
# pip install djangorestframework
# settings.py: INSTALLED_APPS += ['rest_framework']
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from rest_framework.views import APIView
# ── @api_view decorator (function-based) ──────────────────────
@api_view(["GET", "POST"])
def product_list(request):
if request.method == "GET":
products = [
{"id": 1, "name": "Widget", "price": 9.99},
{"id": 2, "name": "Gadget", "price": 24.99},
]
return Response(products) # JSON or browsable HTML — auto content-negotiated
# POST — request.data is already parsed (JSON, form, multipart)
# No json.loads() needed, no Content-Type check needed
name = request.data.get("name")
price = request.data.get("price")
if not name or price is None:
return Response(
{"errors": {"name": ["Required"], "price": ["Required"]}},
status=status.HTTP_400_BAD_REQUEST,
)
# ... save ...
return Response({"id": 99, "name": name, "price": price}, status=status.HTTP_201_CREATED)
# ── APIView class-based ────────────────────────────────────────
class ProductDetail(APIView):
def get(self, request, pk):
# ... fetch product by pk ...
product = {"id": pk, "name": "Widget", "price": 9.99}
return Response(product)
def put(self, request, pk):
# request.data: dict parsed from JSON body
updated = {
"id": pk,
"name": request.data.get("name", ""),
"price": request.data.get("price", 0),
}
return Response(updated)
def delete(self, request, pk):
# ... delete ...
return Response(status=status.HTTP_204_NO_CONTENT) # No body for 204
# ── urls.py ────────────────────────────────────────────────────
# from django.urls import path
# urlpatterns = [
# path("api/products/", product_list),
# path("api/products/<int:pk>/", ProductDetail.as_view()),
# ]
# ── Authentication and permissions ────────────────────────────
from rest_framework.permissions import IsAuthenticated
from rest_framework.authentication import TokenAuthentication
class ProtectedView(APIView):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request):
# request.user is set by TokenAuthentication
return Response({"user": request.user.username, "message": "Hello!"})
# ── DRF settings (REST_FRAMEWORK in settings.py) ──────────────
# REST_FRAMEWORK = {
# "DEFAULT_RENDERER_CLASSES": [
# "rest_framework.renderers.JSONRenderer",
# "rest_framework.renderers.BrowsableAPIRenderer", # remove in production
# ],
# "DEFAULT_AUTHENTICATION_CLASSES": [
# "rest_framework.authentication.TokenAuthentication",
# ],
# "DEFAULT_PERMISSION_CLASSES": [
# "rest_framework.permissions.IsAuthenticated",
# ],
# }DRF's Response differs from JsonResponse in one important way: it does not serialize data immediately at construction time. It defers rendering until after view execution — the Django response middleware triggers the renderer selection and calls renderer.render(data). This enables content negotiation and allows DRF's exception handlers to intercept unhandled exceptions and convert them to structured JSON error responses before rendering. The browsable API renderer — HTML with an interactive POST form — is automatically used when a browser visits a DRF endpoint, making manual API testing trivial during development.
ModelSerializer: Generating JSON Serializers from Models
ModelSerializer is DRF's primary tool for converting Django model instances to JSON and back. It introspects the model's field definitions and generates serializer fields automatically. The Meta.model attribute points to the Django model; Meta.fields is either '__all__' (all fields) or a list of field names. serializer.data is the JSON-ready dict; serializer.save() calls create() or update() depending on whether an instance was passed to the serializer constructor.
# models.py
from django.db import models
class Category(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
class Meta:
verbose_name_plural = "categories"
class Product(models.Model):
category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name="products")
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
stock = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# serializers.py
from rest_framework import serializers
from .models import Product, Category
# ── Minimal ModelSerializer — 5 lines ─────────────────────────
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = "__all__" # id, name, slug
# ── Explicit fields — preferred for production ────────────────
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ["id", "name", "description", "price", "stock", "is_active", "category", "created_at"]
# read_only_fields excludes from input validation; still included in output
read_only_fields = ["id", "created_at"]
# What serializer.data looks like for a product instance:
# {
# "id": 1,
# "name": "Widget",
# "description": "A small widget",
# "price": "9.99", ← Decimal serialized as string by default
# "stock": 42,
# "is_active": true,
# "category": 3, ← ForeignKey as PrimaryKey integer by default
# "created_at": "2026-05-28T10:00:00Z"
# }
# ── views.py: Full CRUD with ModelSerializer ──────────────────
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .models import Product
from .serializers import ProductSerializer
@api_view(["GET", "POST"])
def product_list(request):
if request.method == "GET":
products = Product.objects.filter(is_active=True)
serializer = ProductSerializer(products, many=True) # many=True for querysets
return Response(serializer.data)
# POST — create a new product
serializer = ProductSerializer(data=request.data)
if serializer.is_valid():
serializer.save() # calls ModelSerializer.create()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@api_view(["GET", "PUT", "PATCH", "DELETE"])
def product_detail(request, pk):
try:
product = Product.objects.get(pk=pk)
except Product.DoesNotExist:
return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND)
if request.method == "GET":
serializer = ProductSerializer(product)
return Response(serializer.data)
if request.method in ("PUT", "PATCH"):
partial = request.method == "PATCH" # PATCH: only validate provided fields
serializer = ProductSerializer(product, data=request.data, partial=partial)
if serializer.is_valid():
serializer.save() # calls ModelSerializer.update()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# DELETE
product.delete()
return Response(status=status.HTTP_204_NO_CONTENT)ModelSerializer maps Django model fields to serializer fields: CharField → CharField, IntegerField → IntegerField, DecimalField → DecimalField (serialized as a string by default to preserve precision), BooleanField → BooleanField, DateTimeField → DateTimeField (ISO 8601 format), ForeignKey → PrimaryKeyRelatedField (integer ID). Override fields by declaring them explicitly on the serializer class — they take precedence over the auto-generated fields. Use extra_kwargs in Meta to modify auto-generated field options without redeclaring: extra_kwargs = {'{"price": {"min_value": 0}}'}.
Nested Serializers and Related Model Fields
By default, ModelSerializer represents ForeignKey fields as integer primary keys. To embed the related object as a nested JSON object, declare a nested serializer on the same field name. For writable nested relations, override create() and update() — DRF does not save nested relations automatically to avoid ambiguous behavior. For reverse relations (one-to-many), use many=True and the related_name or default <model>_set name.
from rest_framework import serializers
from .models import Product, Category
# ── Read-only nested serializer ────────────────────────────────
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ["id", "name", "slug"]
class ProductWithCategorySerializer(serializers.ModelSerializer):
# Declare nested serializer with same name as the ForeignKey field
# read_only=True: included in output, excluded from input validation
category = CategorySerializer(read_only=True)
class Meta:
model = Product
fields = ["id", "name", "price", "category", "created_at"]
# Output:
# {
# "id": 1,
# "name": "Widget",
# "price": "9.99",
# "category": { "id": 3, "name": "Electronics", "slug": "electronics" },
# "created_at": "2026-05-28T10:00:00Z"
# }
# ── Writable nested with create() override ────────────────────
class WritableProductSerializer(serializers.ModelSerializer):
category = CategorySerializer() # no read_only — included in input
class Meta:
model = Product
fields = ["id", "name", "price", "category"]
def create(self, validated_data):
category_data = validated_data.pop("category")
# get_or_create: find existing or create new category
category, _ = Category.objects.get_or_create(
slug=category_data["slug"],
defaults=category_data,
)
return Product.objects.create(category=category, **validated_data)
def update(self, instance, validated_data):
category_data = validated_data.pop("category", None)
if category_data:
category, _ = Category.objects.get_or_create(
slug=category_data["slug"],
defaults=category_data,
)
instance.category = category
return super().update(instance, validated_data)
# ── Reverse relation: one Category has many Products ──────────
class CategoryWithProductsSerializer(serializers.ModelSerializer):
# related_name="products" is set on the ForeignKey in Product model
# many=True: serializes a QuerySet
products = ProductWithCategorySerializer(many=True, read_only=True)
class Meta:
model = Category
fields = ["id", "name", "slug", "products"]
# Output:
# {
# "id": 3,
# "name": "Electronics",
# "slug": "electronics",
# "products": [
# { "id": 1, "name": "Widget", "price": "9.99", ... },
# { "id": 2, "name": "Gadget", "price": "24.99", ... }
# ]
# }
# ── SerializerMethodField — computed fields ────────────────────
class ProductSummarySerializer(serializers.ModelSerializer):
in_stock = serializers.SerializerMethodField()
discounted_price = serializers.SerializerMethodField()
class Meta:
model = Product
fields = ["id", "name", "price", "in_stock", "discounted_price"]
def get_in_stock(self, obj):
return obj.stock > 0
def get_discounted_price(self, obj):
# 10% discount for demo
return round(float(obj.price) * 0.9, 2)
# ── Depth shorthand (use sparingly — no field control) ─────────
class ProductDepthSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = "__all__"
depth = 1 # auto-nest all ForeignKey/M2M one level deepNested serializers increase the risk of N+1 queries — if you render a list of 100 products each with a nested category, Django fires 100 separate SQL queries for the category unless you call select_related("category") on the queryset. Always add select_related() for ForeignKey nesting and prefetch_related() for reverse ManyToMany or reverse ForeignKey nesting. Use Django Debug Toolbar in development to count SQL queries per request.
Validating JSON Input with serializer.is_valid()
DRF serializer validation runs in two phases: field-level validation (type coercion, required checks, field constraints from the model like max_length) and object-level validation (cross-field rules in validate() or per-field methods named validate_<fieldname>). Call is_valid(raise_exception=True) to automatically return a 400 response on failure instead of handling the false return manually.
from rest_framework import serializers
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
from .models import Product
# ── Serializer with field-level and object-level validation ────
class ProductCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ["name", "description", "price", "stock", "category"]
# ── Per-field validation: validate_<fieldname> ─────────────
def validate_price(self, value):
if value <= 0:
raise serializers.ValidationError("Price must be greater than 0.")
if value > 99999:
raise serializers.ValidationError("Price cannot exceed 99,999.")
return value
def validate_name(self, value):
# Strip whitespace and check minimum length
value = value.strip()
if len(value) < 2:
raise serializers.ValidationError("Name must be at least 2 characters.")
return value
# ── Object-level validation: validate() ────────────────────
# Receives the full validated field dict; runs after all field validators pass
def validate(self, attrs):
# Cross-field rule: high-stock items must have a description
if attrs.get("stock", 0) > 100 and not attrs.get("description"):
raise serializers.ValidationError(
{"description": "Description is required for high-stock items (stock > 100)."}
)
return attrs
# ── View using is_valid() ─────────────────────────────────────
@api_view(["POST"])
def create_product(request):
serializer = ProductCreateSerializer(data=request.data)
# ── Option 1: manual check ─────────────────────────────────
if not serializer.is_valid():
# serializer.errors: {"field_name": ["error message"], ...}
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
# ── Option 2: raise_exception=True (preferred) ─────────────
# serializer.is_valid(raise_exception=True)
# # DRF exception handler catches ValidationError and returns 400 automatically
# serializer.save()
# return Response(serializer.data, status=status.HTTP_201_CREATED)
# Example 400 error response when name is too short and price <= 0:
# HTTP 400 Bad Request
# {
# "name": ["Name must be at least 2 characters."],
# "price": ["Price must be greater than 0."]
# }
# ── UniqueValidator: auto-added from model unique=True ─────────
# If Product has name = models.CharField(unique=True), DRF adds
# UniqueValidator automatically — no manual code needed.
# 400 response: {"name": ["product with this name already exists."]}
# ── Passing extra data to save() ──────────────────────────────
@api_view(["POST"])
def create_product_for_user(request):
serializer = ProductCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
# Extra kwargs to save() are passed to create() as-is
serializer.save(created_by=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)serializer.errors is a dict mapping field names to lists of error strings — the same structure returned in the 400 response body. Non-field errors (from the validate() method or from non_field_errors) appear under the key "non_field_errors". When using raise_exception=True, DRF's default exception handler converts ValidationError to HTTP 400 with the errors dict as the JSON body — overridable via EXCEPTION_HANDLER in REST_FRAMEWORK settings. The validate() method receives only the valid, coerced fields (fields that failed their own validation are excluded), so check attrs.get() rather than attrs[] when reading fields that might have failed.
Custom JSON Error Responses and Exception Handlers
DRF provides a default exception handler that converts APIException subclasses (including ValidationError, NotFound, PermissionDenied, AuthenticationFailed) into standardized JSON error responses. Override it globally via EXCEPTION_HANDLER in settings to add a consistent error envelope, request IDs, or RFC 7807 Problem Details format. For plain Django views, build your own error helpers.
# ── Plain Django: manual error responses ──────────────────────
import json
from django.http import JsonResponse
def error_response(message, status_code=400, errors=None):
"""Consistent error JSON helper for plain Django views."""
body = {"error": message}
if errors:
body["errors"] = errors
return JsonResponse(body, status=status_code)
def my_view(request):
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return error_response("Invalid JSON body", status_code=400)
if "name" not in data:
return error_response(
"Validation failed",
status_code=400,
errors={"name": ["This field is required."]},
)
# ... process ...
# ── DRF: built-in exception classes ──────────────────────────
from rest_framework.exceptions import (
NotFound, PermissionDenied, ValidationError, AuthenticationFailed
)
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status
@api_view(["GET"])
def get_product(request, pk):
try:
product = Product.objects.get(pk=pk)
except Product.DoesNotExist:
raise NotFound(detail=f"Product {pk} not found.") # → 404 JSON
# DRF default: {"detail": "Product 42 not found."}
if not product.is_active:
raise PermissionDenied(detail="This product is not available.") # → 403
serializer = ProductSerializer(product)
return Response(serializer.data)
# ── DRF: custom exception handler ─────────────────────────────
# custom_exceptions.py
def custom_exception_handler(exc, context):
from rest_framework.views import exception_handler
response = exception_handler(exc, context)
if response is not None:
# Wrap in a consistent envelope
response.data = {
"success": False,
"status_code": response.status_code,
"errors": response.data,
# Optionally add request ID from context
# "request_id": context["request"].META.get("HTTP_X_REQUEST_ID"),
}
return response
# settings.py: register the handler
# REST_FRAMEWORK = {
# "EXCEPTION_HANDLER": "myapp.custom_exceptions.custom_exception_handler",
# }
# With the custom handler, a 400 ValidationError becomes:
# {
# "success": false,
# "status_code": 400,
# "errors": {
# "name": ["This field is required."],
# "price": ["A valid number is required."]
# }
# }
# ── DRF: custom exception for business logic errors ───────────
from rest_framework.exceptions import APIException
class InsufficientStockError(APIException):
status_code = 409 # Conflict
default_detail = "Insufficient stock for this order."
default_code = "insufficient_stock"
@api_view(["POST"])
def place_order(request):
product_id = request.data.get("product_id")
quantity = request.data.get("quantity", 1)
try:
product = Product.objects.get(pk=product_id)
except Product.DoesNotExist:
raise NotFound("Product not found.")
if product.stock < quantity:
raise InsufficientStockError(
detail=f"Only {product.stock} units available, requested {quantity}."
)
# ... process order ...
return Response({"order_id": 12345}, status=status.HTTP_201_CREATED)For RFC 7807 Problem Details format (standard JSON error format for HTTP APIs), the error response body would use type, title, status, detail, and instance fields. Implement this in the custom exception handler by reshaping response.data to the Problem Details schema. The djangorestframework-problem-details package provides a drop-in exception handler for this standard. For production APIs with multiple teams consuming your endpoints, a consistent error format is more valuable than any individual endpoint optimization — standardize early.
Key Terms
- JsonResponse
- A Django
HttpResponsesubclass fromdjango.httpthat serializes a Python dict (or any JSON-serializable value withsafe=False) usingjson.dumps()and setsContent-Type: application/jsonautomatically. Thesafeparameter (defaultTrue) restricts the top-level value to dicts — passsafe=Falsefor lists. Theencoderparameter accepts a customjson.JSONEncodersubclass for non-serializable types likedatetimeandDecimal. Usejson_dumps_paramsto pass options directly tojson.dumps()(e.g.,{"ensure_ascii": False}). - request.body
- A
bytesattribute on Django'sHttpRequestobject that contains the raw request body. For JSON APIs, pass it tojson.loads(request.body)to decode to a Python dict.request.bodyis readable multiple times in a single request (Django buffers it). It is empty ifrequest.POSThas already been accessed — Django reads the body stream for form data. In DRF views, userequest.datainstead — it automatically parses JSON, form data, and multipart based onContent-Type. - ModelSerializer
- A DRF serializer class that introspects a Django model at class definition time and auto-generates serializer fields matching the model's field definitions. Declare
Meta.modelandMeta.fieldsto produce a bidirectional converter between model instances and Python dicts (JSON-ready).serializer.data(output) is the dict representation;serializer.save()callscreate()orupdate()depending on whether an instance was passed.many=Truewraps the serializer in aListSerializerfor querysets and lists. Overridecreate()andupdate()for writable nested relations. - request.data
- A DRF property on the augmented request object that returns the parsed request body as a Python dict-like object. DRF selects the parser based on the
Content-Typeheader:JSONParserforapplication/json,FormParserfor URL-encoded form data,MultiPartParserfor multipart file uploads. Unlikerequest.body(bytes) andrequest.POST(form-only),request.datahandles all content types transparently. Accessing an absent key returnsNonerather than raisingKeyError— use.get()for optional fields. - is_valid()
- A DRF serializer method that runs field-level and object-level validation on the input data and returns
True(valid) orFalse(invalid). On success,serializer.validated_datacontains type-coerced, validated values. On failure,serializer.errorsis a dict mapping field names to lists of error message strings. Passraise_exception=Trueto automatically raiseValidationErroron failure — DRF's exception handler converts it to an HTTP 400 JSON response without additional code. Never accessserializer.validated_databefore callingis_valid()— it raisesAssertionError. - APIView
- The DRF base class for class-based API views, wrapping Django's
View. Definesget(),post(),put(),patch(), anddelete()methods that receive a DRF-augmentedrequestobject (withrequest.data,request.user,request.auth) and return DRFResponseobjects. Dispatches to the correct method handler, applies authentication and permission checks before the handler runs, handlesAPIExceptionsubclasses, and triggers content negotiation for the response. Use@api_viewdecorator for function-based equivalents; useViewSetandRouterfor CRUD endpoints with automatic URL registration.
FAQ
How do I return a JSON response in Django?
Use JsonResponse(data) from django.http. Pass a Python dict as the first argument — Django automatically sets Content-Type: application/json and serializes using json.dumps() with no manual encoding needed. For example: return JsonResponse({"status": "ok", "count": 42}). The default status code is 200; pass status=201 or any integer for other codes. For lists as the top-level value, use safe=False: return JsonResponse(results, safe=False). For non-serializable types like datetime or Decimal, pass a custom encoder: return JsonResponse(data, encoder=MyEncoder). In Django REST Framework, use Response(data) from rest_framework.response instead — it supports content negotiation and returns JSON by default for API clients.
Why does Django's JsonResponse require safe=False for lists?
By default, JsonResponse only accepts Python dicts as the top-level value (safe=True). This was a historical security measure from Django 1.6 to guard against a JSON array hijacking attack that affected older browsers — a CSRF-style exploit where a malicious page could override the JavaScript Array constructor and steal JSON array data via a <script> tag. Modern browsers (all 2011+) fixed this vulnerability, but the default remains for backward compatibility. Pass safe=False explicitly when your top-level JSON value is a list: return JsonResponse(items_list, safe=False). Django REST Framework's Response() has no such restriction — it accepts lists, dicts, and any JSON-serializable value without a safe flag, making it more ergonomic for REST APIs where paginated list responses are common.
How do I read a JSON request body in Django?
In plain Django views, read the raw body bytes from request.body and decode with json.loads(): data = json.loads(request.body). Always wrap in try/except json.JSONDecodeError — return a 400 response if the body is malformed: return JsonResponse({"error": "Invalid JSON"}, status=400). Do not access request.POST before request.body in the same view — Django reads the body stream for form data, leaving request.body empty. In Django REST Framework, use request.data instead — it automatically parses JSON bodies (and form data, multipart) based on the Content-Type header with no manual decoding. request.data is the DRF-recommended approach for all API views; use json.loads(request.body) only in plain Django views where DRF is not installed.
What is Django REST Framework and when should I use it?
Django REST Framework (DRF) is a third-party package (pip install djangorestframework) that adds a complete REST API toolkit on top of Django: automatic JSON serialization with ModelSerializer, content negotiation with Response(), authentication classes (Token, JWT, Session, OAuth), permission classes (IsAuthenticated, custom), throttling, cursor and offset pagination, filtering, and a browsable API HTML interface for development. Use plain Django JsonResponse for 1-3 simple JSON endpoints with no content negotiation or model serialization requirements. Switch to DRF when you have 4 or more endpoints, need ModelSerializerfor database-backed resources, need authentication and permission enforcement, need standardized list pagination, or want a browsable API during development. DRF adds negligible per-request overhead in production and is used in major Python projects including Instagram's early API and Mozilla services.
How does DRF ModelSerializer generate JSON automatically?
ModelSerializer introspects the Django model's field definitions at class creation time and generates matching serializer fields. Declare the Meta inner class with model pointing to your Django model and fields = '__all__' (all fields) or an explicit list — that's 5 lines total. DRF maps each model field to a serializer field: CharField → CharField, IntegerField → IntegerField, ForeignKey → PrimaryKeyRelatedField (integer ID), DateTimeField → DateTimeField (ISO 8601 string). serializer.data produces a JSON-ready Python dict from a model instance; serializer.save() creates or updates the database row from validated input. DRF also auto-generates validators from model constraints — unique=True fields get UniqueValidator, max_length is enforced from CharField, and null=True / blank=True control required status automatically.
How do I validate JSON data in Django REST Framework?
Construct a serializer with data=request.data and call serializer.is_valid(). If it returns True, access serializer.validated_data (type-coerced, validated values) and call serializer.save(). If it returns False, serializer.errors is a dict mapping field names to lists of error strings — return it with a 400 status: return Response(serializer.errors, status=400). Use is_valid(raise_exception=True) to skip the if/else entirely: DRF's exception handler automatically converts ValidationError to a 400 JSON response. Add field-level validators with methods named validate_<fieldname> on the serializer class — they receive the field value and must either return it (or a cleaned version) or raise serializers.ValidationError. Add cross-field validation in the validate() method which receives the full attrs dict after all field validators pass.
How do I return a 400 JSON error response in Django?
In plain Django views: return JsonResponse({"error": "Invalid input"}, status=400). For field-level errors, structure them under an "errors" key: return JsonResponse({"errors": {"email": ["Enter a valid email address."]}}, status=400). In DRF views, use Response with status.HTTP_400_BAD_REQUEST: return Response({"detail": "Bad request"}, status=status.HTTP_400_BAD_REQUEST). For serializer validation errors, serializer.errors already has the correct field-keyed structure — return it directly: return Response(serializer.errors, status=400). For 404 errors, raise rest_framework.exceptions.NotFound() which DRF converts to {"detail": "Not found."} with a 404 status. A custom exception handler registered in REST_FRAMEWORK settings can standardize the error envelope — adding success: false, status_code, and a consistent errors key — across all endpoints automatically.
How do I serialize nested related models in DRF?
Declare a nested serializer on the field with the same name as the ForeignKey. For read-only embedding, add read_only=True: author = UserSerializer(read_only=True). The output JSON will contain the full nested object instead of just the integer ID. For reverse one-to-many relations (e.g., a post's comments), use many=True with the related_name from the ForeignKey: comments = CommentSerializer(many=True, read_only=True). For writable nested relations, override create() and update() on the serializer — DRF does not auto-save nested data to avoid ambiguous behavior. Use SerializerMethodField for computed or conditionally-formatted nested values. Always add select_related() to the queryset for ForeignKey nesting and prefetch_related() for reverse or ManyToMany nesting to prevent N+1 query problems — a 100-item list with uneager nesting fires 100 extra SQL queries.
Format and validate your Django API JSON responses
Paste any Django JSON response into Jsonic's formatter to validate the structure, identify issues, and pretty-print for easier debugging.
Open JSON FormatterFurther reading and primary sources
- Django JsonResponse documentation — Official Django docs covering JsonResponse, safe parameter, custom encoders, and json_dumps_params
- Django REST Framework — Serializers — DRF serializer documentation: ModelSerializer, field-level validation, nested serializers, and create/update overrides
- Django REST Framework — Requests — DRF request.data, request.query_params, authentication, and content type parsing
- Django REST Framework — Responses — DRF Response class, content negotiation, status codes, and renderer selection
- Django REST Framework — Exception Handling — Built-in exceptions (NotFound, ValidationError, PermissionDenied) and custom exception handler registration