skip to content

In Django REST Framework, what is the difference between Serializer and ModelSerializer, and when would you choose a plain Serializer?

level: juniorimportance: must knowfreq 78%

answer

  1. who writes the field list
  2. fields and validators from the model
  3. create() and update() for free
  4. payloads that are not one model

basics

~20 s

A plain Serializer declares every field and needs its own create() and update(); a ModelSerializer builds fields and validators from a model through Meta and implements both methods. Choose a plain Serializer when the payload is not one model.

solid answer

~40 s

`ModelSerializer` is a subclass of `Serializer` that inspects the model named in `Meta.model`: it generates a serializer field per model field (with `max_length`, `required`, `allow_null` and read-only flags inferred), adds validators such as uniqueness checks, and implements `create()` and `update()` through the model's default manager. It requires `Meta.fields` or `Meta.exclude`. A plain `Serializer` does none of that: you declare every field, and `save()` raises `NotImplementedError` until you write `create()` or `update()`. I use a plain `Serializer` for payloads that are not one model: a catalog search or price-quote request, a report combining several models, or an input shape that deliberately differs from the table. For a CRUD endpoint over one model, `ModelSerializer` with an explicit `fields` list is the default.

code

python · 16 lines
python
from rest_framework import serializers

from catalog.models import Product


class ProductSerializer(serializers.ModelSerializer):
    class Meta:
        model = Product
        fields = ["id", "sku", "name", "price", "stock"]


class PriceQuoteSerializer(serializers.Serializer):
    # Input for a quote endpoint: not a model, so nothing to save
    sku = serializers.CharField(max_length=32)
    quantity = serializers.IntegerField(min_value=1)
    coupon = serializers.CharField(required=False, allow_blank=True)

go deeper

for a junior

Know that ModelSerializer builds fields from Meta.model and needs fields or exclude, while a plain Serializer lists every field and needs its own create() and update().

for a middle

Explain what ModelSerializer infers from model fields, how save() chooses between create() and update(), and how to inspect generated fields with repr().

for a senior

Decide per endpoint whether the contract should follow the model or be declared by hand, and spot generated behaviour that leaks model changes into the API.

for a principal

Weigh generated serializers against hand-declared contracts across a codebase: speed of CRUD versus an API that stays stable while the schema evolves.

## Two classes, one base In Django REST Framework (DRF) a **serializer** converts in both directions: model instances or other Python objects into primitive data for the response, and incoming request data into validated Python values. `serializers.Serializer` is the general class; `serializers.ModelSerializer` is a **subclass** of it that knows how to read a Django model. Everything a plain `Serializer` can do, a `ModelSerializer` can also do; the difference is how much is generated for you. | Aspect | `Serializer` | `ModelSerializer` | |---|---|---| | Field list | declared by hand, one attribute per field | generated from `Meta.model`, filtered by `Meta.fields` or `Meta.exclude` | | Field options | written by you | inferred from each model field: `max_length`, `required`, `allow_null`, read-only | | Validators | only what you declare | adds model-derived ones, such as uniqueness checks | | `create()` / `update()` | raise `NotImplementedError` until you write them | implemented through the model's default manager | | `Meta` | not used | `model` plus `fields` or `exclude` is required | ## What ModelSerializer generates For a catalog `Product` model with `sku`, `name`, `price`, `stock`, a nullable `category` foreign key and an `auto_now_add` `created_at`, a `ModelSerializer` produces: - a `CharField` for `sku` with the model's `max_length`, and a `DecimalField` for `price` with its `max_digits` and `decimal_places`; - a **read-only** `id` (the `AutoField` primary key) and a read-only `created_at`, because `auto_now_add` makes the model field non-editable; - `required=False` wherever the model field has a default, `blank=True` or `null=True`; - a primary-key relation field for `category`; - a default `create()` that separates many-to-many values, calls `Product._default_manager.create(**validated_data)`, then sets the many-to-many relations, and a default `update()` that sets attributes and calls `save()`. The write happens immediately when `save()` runs; there is no deferred flush at the end of the request. ## When a plain Serializer is the right choice - **Input that is not a model**: a catalog search request, a price-quote request with `sku`, `quantity` and a coupon code, or a bulk import command. - **Output assembled from several sources**: a dashboard summary built from aggregates of several models. - **A deliberately different shape**: when the API contract must not follow the table layout, declaring fields by hand keeps the contract independent of model changes. - **Data from outside the database**: payloads for or from an external service. In each case you declare the fields yourself and, if the endpoint writes anything, implement `create()` and/or `update()`. `save()` calls `create(validated_data)` when the serializer has no instance and `update(instance, validated_data)` when it has one. ## Mixing the two The classes are not an either-or choice inside one endpoint: - A `ModelSerializer` can **declare extra fields** next to the generated ones, for example a `coupon` code that is not a model column. Such a field must be listed in `fields`, and it must be removed from `validated_data` before the default `create()` passes the rest to the model, or the model constructor rejects the unknown keyword. - A `ModelSerializer` can **override `create()` or `update()`** while keeping its generated fields, for example to record which user created a product. - A plain `Serializer` can **contain** a `ModelSerializer` as a field, so an import request can carry a list of products plus a batch label. The rule of thumb: start from `ModelSerializer` when the payload is one model, and drop to `Serializer` when the model would have to be bent to fit the contract. ## Seeing what was generated A `ModelSerializer`'s generated fields are not visible in the class body. `repr()` prints them: 1. open `python manage.py shell`; 2. instantiate the serializer, for example `ProductSerializer()`; 3. print its `repr()`; every declared and generated field is listed with its keyword arguments. This is the quickest way to answer "why is this field required?" or "why is this field read-only?". ## Common mistakes - Leaving out both `fields` and `exclude`: DRF raises an `AssertionError` telling you to add `fields = '__all__'` explicitly; it does not silently expose every field. - Assuming `ModelSerializer` is only for output. It validates input exactly like a plain `Serializer`, plus the model-derived rules. - Writing a plain `Serializer`, calling `save()`, and being surprised by a `NotImplementedError` whose message says `create()` must be implemented. - Reaching for a plain `Serializer` for a simple one-model CRUD endpoint and re-typing every field, which drifts from the model as it changes.

  • How do you see which fields a DRF ModelSerializer generated?
    Instantiate it in `python manage.py shell` and print its `repr()`. DRF lists every declared and generated field with its keyword arguments, such as `read_only=True`, `required=False` or `max_length=32`, which is the fastest way to explain an unexpected validation error or a field that will not accept input.
  • Can a plain DRF Serializer write to the database?
    Yes. Implement `create(self, validated_data)` and, for edits, `update(self, instance, validated_data)`, each returning the instance. `save()` calls `create()` when no instance was passed to the serializer and `update()` when one was. Without them, `save()` raises `NotImplementedError`.

saying these in an interview costs you the question

  • A ModelSerializer with no Meta.fields exposes every model field by default.
  • A plain Serializer is only for output; input always needs a ModelSerializer.
  • ModelSerializer skips validation because the model already validates.
  • ModelSerializer defers the database write until the request finishes.
  • Calling save() on a plain Serializer inserts a row automatically.