Serializers: turning a Django model into JSON
A serializer is DRF's translator between a Django model instance and JSON, in both directions: model → JSON for a response, and JSON → validated Python data for a request. This part writes one by hand, field by field — the shortcut that generates this automatically arrives in part 3, but seeing the manual version first is what makes the shortcut make sense.
A plain Serializer
# tasks/serializers.py
from rest_framework import serializers
class TaskSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
title = serializers.CharField(max_length=200)
description = serializers.CharField(required=False, allow_blank=True)
completed = serializers.BooleanField(default=False)
due_date = serializers.DateField(required=False, allow_null=True)Every field mirrors a model field, but declared separately — a serializer field is about the JSON shape and validation rules, not storage. read_only=True on id means it appears in output but is ignored on input, since a client never gets to choose a task's ID.
Serializing: model to JSON
from rest_framework.renderers import JSONRenderer
from tasks.models import Task
task = Task.objects.first()
serializer = TaskSerializer(task)
serializer.data
# {'id': 1, 'title': 'Write the API', 'description': '', 'completed': False, 'due_date': None}
JSONRenderer().render(serializer.data)
# b'{"id":1,"title":"Write the API","description":"","completed":false,"due_date":null}'Passing a model instance as the first argument puts the serializer in "output" mode — .data is an OrderedDict of plain Python values, not JSON text yet. JSONRenderer is the separate step that turns it into actual bytes; a real view (starting part 4) handles that automatically, but seeing it as two distinct steps here is what makes the "serializer versus renderer" split make sense.
Deserializing: JSON to validated data
data = {"title": "Ship part 2", "completed": False}
serializer = TaskSerializer(data=data)
serializer.is_valid() # True
serializer.validated_data
# OrderedDict([('title', 'Ship part 2'), ('completed', False)])Passing data= instead puts the serializer in "input" mode. .is_valid() runs every field's validation — required fields present, types correct, max_length respected — and only after it returns True is .validated_data safe to read, the same is_valid() → cleaned_data/validated_data shape Django's own Form uses.
What .is_valid() catches
bad = TaskSerializer(data={"description": "no title"})
bad.is_valid() # False
bad.errors
# {'title': [ErrorDetail(string='This field is required.', code='required')]}title has no default and isn't required=False, so DRF requires it — the same field-declaration rules that build the browsable API's HTML form (part 18) are exactly what produces this error message.
Reading serializer.data on a serializer constructed with data= before checking .is_valid(). On an input-mode serializer, .validated_data is what's safe after validation — .data in that mode can raise or return something unexpected, since it's designed for the output direction.
Next: ModelSerializer — the same result as this hand-written class, generated from the model in a few lines instead of one per field.