In a Django REST Framework ModelSerializer, how do Meta.fields, read_only_fields and extra_kwargs decide which model fields are exposed and writable?
answer
- allow-list first
- what the model already implies
- shortcuts built on extra_kwargs
- declared fields ignore Meta options
basics
~20 sMeta.fields (or exclude) chooses which model fields exist on the serializer; read_only_fields marks some as output-only; extra_kwargs passes any other field argument. Both shortcuts apply only to generated fields and are ignored for fields declared explicitly on the class.
solid answer
~40 s`Meta.fields` is an explicit list, or `'__all__'`; `Meta.exclude` is the alternative, and DRF asserts that you set exactly one. On top of that, DRF infers options from the model: the `AutoField` primary key and any `editable=False` field, such as `auto_now_add`, become read-only, and a default, `blank=True` or `null=True` makes a field not required. `read_only_fields` is a list or tuple of names to make read-only, implemented through `extra_kwargs`, which passes arbitrary keyword arguments such as `{"sku": {"min_length": 6}}` to generated fields. The trap: both options apply only to fields DRF generates. A field declared on the class, such as `slug = serializers.SlugField()`, ignores them, so it must carry `read_only=True` itself. And every declared field must also appear in `fields`, or DRF raises an `AssertionError`.
code
python · 14 linesfrom rest_framework import serializers
from catalog.models import Product
class ProductSerializer(serializers.ModelSerializer):
slug = serializers.SlugField(read_only=True) # declared: Meta options do not reach it
class Meta:
model = Product
fields = ["id", "sku", "name", "slug", "price", "stock", "created_at"]
read_only_fields = ["stock"] # generated field: becomes read-only
extra_kwargs = {"sku": {"min_length": 6}}
# id (AutoField) and created_at (auto_now_add) are read-only without being listedgo deeper
Remember that Meta.fields lists what the serializer exposes, that read_only_fields makes fields output-only, and that the primary key is read-only automatically.
Explain what DRF infers from model fields, how read_only_fields is built on extra_kwargs, and why neither affects a field declared on the class.
Review serializers for 'all' and exclude drift, catch declared fields that silently stay writable, and debug field behaviour with repr().
Set the team convention: explicit fields lists, declared fields carrying their own flags, and review rules that keep new columns out of the API by default.
## Choosing the fields A `ModelSerializer` starts from the model named in `Meta.model` and needs to be told which of its fields to use: | Option | Meaning | Notes | |---|---|---| | `fields = ["id", "sku", ...]` | exactly these, in this order | the usual choice; new model fields stay out until added | | `fields = "__all__"` | every model field | a new column becomes part of the API, and writable, the day it is added | | `exclude = ["cost_price"]` | every field except these | same drift as `"__all__"` for anything not excluded | DRF enforces the rules: setting **both** `fields` and `exclude` fails an assertion, setting **neither** fails an assertion that asks for an explicit `fields = '__all__'`, and `fields` must be a list, a tuple or the string `'__all__'` or a `TypeError` is raised. Any field **declared on the class** must also be named in `fields`, unless it was declared on a parent serializer, or DRF raises an `AssertionError` naming the missing field. ## What DRF infers from each model field Before any `Meta` option is applied, DRF maps each model field to a serializer field and derives its arguments: - The auto-incrementing primary key is read-only: an **`AutoField`**, and `BigAutoField`, the Django 6.x default, counts as one. - Any field with **`editable=False`** is read-only; Django sets that on `auto_now` and `auto_now_add` date fields, so a `created_at` timestamp needs no extra configuration. - A field with a **default**, `blank=True` or `null=True` gets `required=False`. - `null=True` adds `allow_null=True`; `blank=True` on a text field adds `allow_blank=True`. - `max_length`, `max_digits`, `decimal_places` and choices carry over. - A unique model field gets a uniqueness validator; how those validators behave is a validation subject of its own. `repr()` of a serializer instance shows the result, which is the fastest way to see what DRF decided. ## read_only_fields `read_only_fields` is a shortcut for marking several generated fields read-only without declaring them: - It must be a **list or tuple**; anything else raises a `TypeError`. - It is implemented by adding `read_only: True` to each named field's entry in `extra_kwargs`. - A read-only field appears in the output and is **ignored in input**; a client that sends `"stock": 0` for a read-only `stock` changes nothing. - It **does not apply to declared fields**. This is the classic bug: `slug` is declared on the class to customise it, `slug` is listed in `read_only_fields`, and it stays writable. ## extra_kwargs `extra_kwargs` maps a field name to extra keyword arguments for the generated field, so you can tweak it without re-declaring it: - `{"sku": {"min_length": 6}}` adds a constraint the model does not have. - `{"stock": {"read_only": True}}` is what `read_only_fields` does internally. When `read_only` is set this way, DRF drops arguments that only make sense for input, such as `required`, `default` and `validators`. - Like `read_only_fields`, it is **ignored for declared fields**; the declared field's own constructor arguments are the only ones that count. ## Reading the example serializer For the catalog `ProductSerializer` shown with this question, DRF ends up with: - `id` and `created_at`: read-only because of the model, with no `Meta` entry. - `stock`: read-only because of `read_only_fields`; clients see it but cannot change it through this serializer. - `sku`: writable, with the model's `max_length` plus the extra `min_length=6` from `extra_kwargs`. - `slug`: read-only only because its declaration says `read_only=True`; listing it in `read_only_fields` would change nothing. - `name` and `price`: writable, and required unless the model gives them a default, `blank=True` or `null=True`. ## Debugging a field that behaves unexpectedly 1. Print `repr(ProductSerializer())` and read the field's arguments. 2. Check whether the field is **declared on the class** (or a parent); if so, no `Meta` option affects it. 3. Check the model field: `editable=False`, a default, `blank` or `null` explain read-only and not-required flags. 4. Check whether `fields = "__all__"` or `exclude` pulled in a field nobody meant to expose. The design rule that follows is simple: prefer an explicit `fields` list, keep `read_only_fields` and `extra_kwargs` for generated fields, and put `read_only=True` directly on any field you declare yourself. Why allow-lists beat deny-lists as a general principle is covered elsewhere; here the point is where DRF applies each option and where it silently does not.
- Why does listing a declared field in a DRF ModelSerializer's read_only_fields have no effect?`read_only_fields` is implemented through `extra_kwargs`, and DRF applies `extra_kwargs` only while building fields from the model. A declared field is taken as-is from the class, so the Meta options never reach it. Put `read_only=True` in the declared field's constructor instead.
- What does DRF do when a ModelSerializer declares a field but Meta.fields does not list it?It raises an `AssertionError` saying the field was declared on the serializer but not included in the `fields` option. The check skips fields declared on a parent serializer, so a subclass may narrow `fields` to a subset of its parent's fields.
saying these in an interview costs you the question
- read_only_fields also makes a field declared on the class read-only.
- Without Meta.fields a ModelSerializer quietly includes every model field.
- created_at with auto_now_add must be added to read_only_fields to stop writes.
- A field with null=True becomes read-only on the serializer.
- extra_kwargs overrides the arguments of explicitly declared fields.