DRF Serializers
A serializer is two-way: it serializes model instances → primitive dicts (then JSON) and deserializes request payloads → validated Python data → model instances. It’s also where validation lives.
Three flavors
| Class | Use when |
|---|---|
Serializer |
Hand-built fields; not tied to a model. Good for action payloads, search params, login forms. |
ModelSerializer |
Auto-generates fields + create()/update() from a model. 90% of CRUD endpoints. |
HyperlinkedModelSerializer |
Same as ModelSerializer but FKs render as URLs instead of pks. Rarely used in practice. |
ModelSerializer in 5 lines
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ["id", "title", "author", "published_at"]
read_only_fields = ["id", "published_at"]
What it generates for you:
- A field per model field, type-mapped (
CharField→CharField,ForeignKey→PrimaryKeyRelatedField). - A
create()that doesModel.objects.create(**validated_data). - An
update()that doessetattr+instance.save(). - Validators: model-level
unique,unique_together,max_length,MinValueValidator, etc.
fields vs exclude vs __all__
fields = ["id", "title"] # explicit allowlist — preferred
fields = "__all__" # every model field — risky, leaks new columns automatically
exclude = ["secret_key"] # blocklist — same risk in reverse
Always prefer an explicit fields list. __all__ is the source of “we accidentally exposed password_hash after a migration” stories.
Read/write control per field
class UserSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True) # never returned
full_name = serializers.SerializerMethodField() # implicitly read-only
class Meta:
model = User
fields = ["id", "email", "password", "full_name"]
extra_kwargs = {"email": {"required": True}}
def get_full_name(self, obj):
return f"{obj.first_name} {obj.last_name}"
read_only=True→ only in output, ignored on input.write_only=True→ only on input, never serialized back (passwords, OTPs).SerializerMethodFieldis always read-only — for computed values.
The .is_valid() / .save() / .data flow
serializer = BookSerializer(data=request.data)
serializer.is_valid(raise_exception=True) # populates .validated_data or raises ValidationError → 400
serializer.save(owner=request.user) # extra kwargs are merged into validated_data
return Response(serializer.data, status=201)
Pitfall: serializer.data is only valid after is_valid() (and after save() if writing). If you access .data before is_valid() on a write serializer, you get initial_data, not validated data.
to_representation and to_internal_value
The escape hatches when fields aren’t enough.
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = ["id", "name"]
def to_representation(self, instance):
# Output side: "python" instead of {"id": 1, "name": "python"}
return instance.name
def to_internal_value(self, data):
# Input side: accept "python" string and resolve to a Tag
return Tag.objects.get_or_create(name=data)[0]
Use sparingly — overriding both bypasses the field-by-field validation chain.
Context — passing extra info into serialization
serializer = BookSerializer(book, context={"request": request})
# Inside the serializer:
def get_url(self, obj):
return self.context["request"].build_absolute_uri(obj.get_absolute_url())
Generic views auto-inject {"request": ..., "view": ..., "format": ...} into context. If you instantiate manually (e.g. nested serialization, signals, scripts), you must pass it.
many=True — the ListSerializer wrapper
serializer = BookSerializer(Book.objects.all(), many=True)
Behind the scenes DRF wraps the serializer in a ListSerializer. To customize bulk behavior (bulk create, bulk update), set Meta.list_serializer_class = MyListSerializer and override create() there.
Multiple serializers per view
Common pattern: lightweight list, heavy detail, write-only create.
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.select_related("author")
def get_serializer_class(self):
if self.action == "list":
return BookListSerializer # only id, title
if self.action in ("create", "update", "partial_update"):
return BookWriteSerializer # accepts author_id
return BookDetailSerializer # full nested representation
Interview angle
- “
SerializervsModelSerializer— when would you reach for plainSerializer?” — non-model payloads (search filters, login, action bodies, aggregations). - “Difference between
read_only,write_only, andSerializerMethodField?” — output-only / input-only / always-read-only-and-computed-by-a-method. - “What does
ModelSerializerdo under the hood that you’d lose with plainSerializer?” — auto field generation fromModel._meta, defaultcreate/update,unique/max_lengthvalidators, FK →PrimaryKeyRelatedField. - “Why is
fields = '__all__'discouraged?” — silent surface expansion when migrations add columns; leaks sensitive fields by default. - “What’s in
serializer.contextand who puts it there?” —request,view,format; injected byGenericAPIView.get_serializer_context(). Manual instantiations must pass it.