DRF Serializers

5 interview angles 3 min read source

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 (CharFieldCharField, ForeignKeyPrimaryKeyRelatedField).
  • A create() that does Model.objects.create(**validated_data).
  • An update() that does setattr + 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).
  • SerializerMethodField is 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

  • Serializer vs ModelSerializer — when would you reach for plain Serializer?” — non-model payloads (search filters, login, action bodies, aggregations).
  • “Difference between read_only, write_only, and SerializerMethodField?” — output-only / input-only / always-read-only-and-computed-by-a-method.
  • “What does ModelSerializer do under the hood that you’d lose with plain Serializer?” — auto field generation from Model._meta, default create/update, unique/max_length validators, FK → PrimaryKeyRelatedField.
  • “Why is fields = '__all__' discouraged?” — silent surface expansion when migrations add columns; leaks sensitive fields by default.
  • “What’s in serializer.context and who puts it there?”request, view, format; injected by GenericAPIView.get_serializer_context(). Manual instantiations must pass it.