In Django REST Framework, what is the difference between Serializer and ModelSerializer, and when would you choose a plain Serializer?
answer
- who writes the field list
- fields and validators from the model
- create() and update() for free
- payloads that are not one model
basics
~20 sA 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 linesfrom 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
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().
Explain what ModelSerializer infers from model fields, how save() chooses between create() and update(), and how to inspect generated fields with repr().
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.
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.