In a Django REST Framework ModelSerializer, what does Meta.depth do, and why are the nested fields it generates read-only?
answer
- relations expanded instead of pks
- an auto-built serializer per level
- fields = '__all__' underneath
- the generated field's kwargs
basics
~20 sMeta.depth, from 0 to 10, makes a ModelSerializer render relations as nested objects instead of primary keys, auto-building a nested ModelSerializer with fields 'all' per level. Those generated fields are read_only=True, so clients can no longer set the relation.
solid answer
~40 s`depth = 1` tells `ModelSerializer` to build each relation field as an auto-generated nested `ModelSerializer` (model = the related model, `fields = '__all__'`, `depth` one lower) instead of a `PrimaryKeyRelatedField`; `depth` must be between 0 and 10. The generated fields are created with `read_only=True` (plus `many=True` for to-many relations), because DRF will not guess how to create or update related rows. So the relation drops out of input entirely: a POST can no longer set the foreign key, and a non-nullable one fails at the database unless you pass it through `save()`. Depth is also all-or-nothing: every relation in `fields` expands, exposing every column of the related model. For control, declare the nested serializer yourself and add a separate write field, such as a `PrimaryKeyRelatedField` with `source=` and `write_only=True`.
code
python · 20 linesfrom rest_framework import serializers
from shop.models import Customer, Order
class CustomerSummarySerializer(serializers.ModelSerializer):
class Meta:
model = Customer
fields = ["id", "display_name"]
class OrderSerializer(serializers.ModelSerializer):
customer = CustomerSummarySerializer(read_only=True) # output
customer_id = serializers.PrimaryKeyRelatedField( # input
source="customer", queryset=Customer.objects.all(), write_only=True
)
class Meta:
model = Order
fields = ["id", "note", "customer", "customer_id"]go deeper
Remember that depth swaps primary keys for nested objects in the response and that the nested part is read-only.
Explain how build_nested_field generates a NestedSerializer with fields 'all' and depth minus one, why its kwargs carry read_only=True, and what that does to POST.
Push back on depth for public APIs: it leaks every column, cannot be tuned per relation and changes with the model. Declare nested output plus a write-only id field instead.
Weigh convenience against contract stability: depth couples the API to the model schema, so any team policy should say where it is acceptable, such as internal read-only endpoints.
## What `depth` does A Django REST Framework (DRF) `ModelSerializer` inspects its model and generates a serializer field for each name in `Meta.fields`. For a **relation** — a `ForeignKey`, `OneToOneField`, `ManyToManyField` or a reverse relation you list by name — the default generated field is a `PrimaryKeyRelatedField`, so the payload carries the related object's id. `Meta.depth` changes that. It is an integer saying how many levels of relations to **expand into nested objects** before falling back to primary keys. DRF asserts that it is between **0 and 10**: a negative value fails with `'depth' may not be negative.` and anything above 10 with `'depth' may not be greater than 10.` ## How the nested fields are generated When `depth` is above zero, `ModelSerializer.build_field()` sends each relation to `build_nested_field()`, which does three things: 1. It defines a throwaway class `NestedSerializer(ModelSerializer)` whose `Meta` has `model` set to the related model, `fields = '__all__'` and `depth = nested_depth - 1`. 2. It builds the field kwargs with `read_only=True`, adding `many=True` when the relation is to-many. 3. It returns that class and those kwargs, so the relation renders as a full object or a list of objects. Because the inner serializer has `depth - 1`, a `depth = 2` order serializer renders its customer and, inside the customer, the customer's own relations. (`HyperlinkedModelSerializer` does the same with a `HyperlinkedModelSerializer` inner class.) ## Why the generated fields are read-only Writing through a nested object is ambiguous: if a client sends `{"customer": {"id": 3, "email": "[email protected]"}}`, should DRF update customer 3, create a new customer, or reject the change? DRF 3 refuses to guess anywhere — the default `ModelSerializer.create()` and `update()` reject writable nested data — and `depth` avoids the question by marking what it generates `read_only=True`. The consequences: - The relation is **ignored on input**; read-only fields are not in the serializer's writable fields, so the value never reaches `validated_data`. - A POST can no longer set the foreign key. For a non-nullable `ForeignKey` the `INSERT` fails with an `IntegrityError` unless the view supplies the value, e.g. `serializer.save(customer=request.user)`. - The generated OpenAPI schema marks the field `readOnly`. ## The costs of `depth` - **Over-exposure.** `fields = '__all__'` on the inner serializer publishes every column of the related model, including ones you never meant to expose (internal flags, password hashes on a user model). - **No per-field control.** Every relation listed in `fields` expands to the same depth; you cannot expand one foreign key and keep another flat. - **Unstable contract.** Adding a column to the related model silently adds it to your API response. - **Query cost.** Each expanded relation is fetched while rendering; that cost and its fix belong to the rendering-cost topic, not to `depth` itself. ## What the response looks like With `depth = 1` on an order serializer whose `customer` points at a custom user model, an (abridged) response might be: ```json {"id": 12, "note": "Gift wrap please", "customer": {"id": 3, "password": "pbkdf2_sha256$...", "last_login": null, "is_superuser": false, "email": "[email protected]"}} ``` The customer's password hash and permission flags are now part of a public payload, simply because the inner serializer uses `'__all__'`. Nobody wrote a line that exposed them, which is exactly why the leak survives code review. ## Declaring the nesting yourself | Approach | Output | Input | Control over fields | |---|---|---|---| | default (`depth = 0`) | pk | pk | n/a | | `Meta.depth = 1` | full related object | none — read-only | none (`'__all__'`) | | declared nested serializer, `read_only=True` | chosen fields | none | full | | declared nested serializer + `*_id` write field | chosen fields | pk | full | The last row is the common production pattern: a read-only nested serializer for output, plus a `PrimaryKeyRelatedField(source='customer', queryset=..., write_only=True)` named, say, `customer_id` for input. A declared field always overrides what `depth` would have generated for that name, so you can also keep `depth` and override the one relation you want to write. Use `depth` for quick internal or read-only endpoints; declare nested serializers for anything that is a public contract.
- With Meta.depth = 1 on an order serializer, how can clients still set the order's customer?Declare a field for it that overrides the generated one — either a writable `PrimaryKeyRelatedField` under the same name, which loses the nested output, or a separate `customer_id` field with `source='customer'` and `write_only=True` next to a read-only nested field. Or have the view set it with `serializer.save(customer=...)`.
- Does Meta.depth expand reverse relations such as order.items?Only when the reverse accessor is listed in `Meta.fields`; `'__all__'` does not include reverse relations. When it is listed, depth expands it like any other relation, as a read-only nested list with `many=True`.
saying these in an interview costs you the question
- Meta.depth makes nested relations writable as long as create() is not overridden.
- Meta.depth lets you pick which relations to expand and which fields to show.
- Meta.depth accepts any positive number and simply stops when relations run out.
- With depth set, clients can still send the related object's pk on POST.