In Django REST Framework, how do you accept an order with nested line items in one POST, and why does the default ModelSerializer.create() refuse it?
answer
- validation works, saving does not
- an assertion names the method
- pop the list, save the parent first
- the child serializer omits its parent
basics
~20 sDeclare items = LineItemSerializer(many=True) and override create(): pop the items, create the Order, then create each LineItem with order=order. The default ModelSerializer.create() raises an AssertionError on writable nested data because DRF will not guess how to persist it.
solid answer
~40 sDeclare the nested field without `read_only`, e.g. `items = LineItemSerializer(many=True)`, and leave `order` out of the child's `fields`, since the parent does not exist yet. `is_valid()` then validates every item; in `validated_data['items']` each item is a dict whose `product` is already a `Product` instance. Override `create()`: pop `items`, create the `Order` from the rest, then create the `LineItem`s with `order=order` — in `transaction.atomic()` so a failure cannot leave half an order. The view adds server-side values with `serializer.save(customer=request.user)`. Without the override, `ModelSerializer.create()` calls `raise_errors_on_nested_writes()`, which asserts: "The `.create()` method does not support writable nested fields by default." DRF 3 made this explicit because nested persistence is ambiguous: save order, create vs. update, and what to do with missing children all depend on the domain.
code
python · 27 linesfrom django.db import transaction
from rest_framework import serializers
from shop.models import LineItem, Order
class LineItemSerializer(serializers.ModelSerializer):
class Meta:
model = LineItem
fields = ["id", "product", "quantity"] # no "order": create() sets it
class OrderSerializer(serializers.ModelSerializer):
items = LineItemSerializer(many=True, min_length=1)
class Meta:
model = Order
fields = ["id", "note", "items"]
def create(self, validated_data):
items_data = validated_data.pop("items")
with transaction.atomic():
order = Order.objects.create(**validated_data)
LineItem.objects.bulk_create(
[LineItem(order=order, **item) for item in items_data]
)
return ordergo deeper
Know that a serializer can be used as a field with many=True, and that saving nested data needs your own create() method.
Explain the validate-then-save split: is_valid() validates each item and resolves related pks to instances, while the default create() asserts against nested data.
Write the create() with the parent saved first, children linked by order=order, the whole write atomic, server-side values passed through save(), and list rules such as min_length.
Decide where order-creation rules live: a serializer override is fine for one entry point, but a service function keeps the API, admin and batch jobs consistent.
## The scenario A shop API wants one request to create an order and its line items: ```json {"note": "Gift wrap please", "items": [{"product": 42, "quantity": 2}, {"product": 57, "quantity": 1}]} ``` The models are an `Order` (with a `customer` foreign key) and a `LineItem` with `order = ForeignKey(Order, related_name="items", on_delete=...)`, `product` and `quantity`. In Django REST Framework (DRF) this takes a **writable nested serializer**: a serializer used as a field of another serializer, *without* `read_only=True`. ## What DRF does by default Declaring `items = LineItemSerializer(many=True)` on `OrderSerializer` already gets you **input validation**: - `is_valid()` runs the child serializer for every element, so each item's `product` pk is resolved by its `PrimaryKeyRelatedField` and each `quantity` is checked. - Errors come back nested under the field name. Since **DRF 3.18** a `many=True` serializer reports them as a dict keyed by the index of each invalid item, e.g. `{"items": {"1": {"product": ["Invalid pk \"99\" - object does not exist."]}}}`; valid items are omitted. - `validated_data["items"]` is a list of dicts in which `product` is a `Product` **instance**, not the raw pk. What you do *not* get is **saving**. `ModelSerializer.create()` first calls `raise_errors_on_nested_writes('create', ...)`, which asserts that no writable nested serializer field has list or dict data in `validated_data`. The assertion message is: *The `.create()` method does not support writable nested fields by default. Write an explicit `.create()` method for serializer ..., or set `read_only=True` on nested serializer fields.* `update()` does the same. An `AssertionError` is not an API exception, so the client sees a server error. ## Why DRF refuses to guess DRF 3 requires these methods to be written explicitly, because the right behaviour depends on the domain: 1. **Save order.** The parent must exist before children can point at it; for a forward foreign key it is the other way round. 2. **Create or update?** A nested object with an `id` might mean "update that row", "link that row", or "reject". 3. **Missing children.** On update, an item absent from the payload might mean "delete it" or "leave it alone". 4. **Side effects.** Stock reservation, price snapshots and totals usually happen here too. Writing `create()` yourself makes those choices visible in code. ## The explicit `create()` The shape is always the same: 1. `items_data = validated_data.pop("items")` — the model constructor does not accept the list. 2. Create the parent from what remains: `Order.objects.create(**validated_data)`. Values passed to `serializer.save(customer=request.user)` are merged into `validated_data`, so the customer arrives here too. 3. Create each child with the parent: `LineItem(order=order, **item)`, one by one or with `bulk_create()` (which skips `save()` and signals). 4. Return the parent; DRF renders it through `to_representation()`, reading `order.items` back. Wrap steps 2-3 in `transaction.atomic()` so a failure on the third item cannot leave an order with two lines. (How `atomic` works is its own topic.) ## The details that bite - **Leave the foreign key to the parent out of the child's fields.** If `LineItemSerializer` listed `order`, a non-nullable foreign key would be *required input* the client cannot know yet. - **Validate the list itself.** A `many=True` serializer accepts an empty list by default; pass `allow_empty=False` or `min_length=1` if an order needs at least one line. - **Keep server-computed fields read-only.** Prices, totals and status should come from the server (`read_only_fields`), not from the payload. - **Do not put business logic only in the serializer if other paths create orders.** A service function called from `create()` keeps the admin, management commands and the API consistent. ## What to test 1. A valid payload creates one order and the expected number of line items, linked to the requesting customer. 2. An invalid item — an unknown product id — returns 400 with the error under `items` at that index, and leaves no order row behind. 3. An empty `items` list is rejected when the rules say an order needs lines. 4. A `customer` value in the payload is ignored, because `customer` is not in the serializer's `fields` and the view supplies it. ## Alternatives | Option | When it fits | |---|---| | explicit `create()` (shown) | the default; clear, testable | | third-party writable-nested packages | many generic nested writes; accept their fixed semantics | | two calls (create order, then POST items) | when items are managed independently | The DRF docs list a third-party package for automatic writable nesting, but most teams keep the explicit override because the rules above are domain decisions.
- What does the client get back when the second of three line items names a product that does not exist?A 400 whose `items` entry is a dict keyed by index, e.g. `{"items": {"1": {"product": ["Invalid pk \"99\" - object does not exist."]}}}` on DRF 3.18. Validation fails before `create()` runs, so nothing is written.
- Why is bulk_create() reasonable here, and what does it skip?It inserts all line items in one query instead of one per item. It does not call each model's `save()` and sends no `pre_save`/`post_save` signals, so any logic living there — a computed line total, say — must move into `create()` or a service function.
- Why not simply mark items read_only=True?Then the items are ignored on input and the default `create()` works, but the order is created with no lines. `read_only=True` is the right choice only when items are written through a separate endpoint.
saying these in an interview costs you the question
- ModelSerializer saves nested line items automatically once they pass validation.
- A writable nested serializer fails at is_valid() unless create() is overridden.
- The line-item serializer must include the order field so items can be linked.
- validated_data holds the raw product ids, so create() must look each product up again.
- The AssertionError about nested writes comes back to the client as a 400 validation error.