skip to content

In Django REST Framework, how does serializer.save() decide between create() and update(), and what do keyword arguments passed to save() do?

level: juniorimportance: should knowfreq 55%

answer

  1. was an instance passed in
  2. merged into validated_data
  3. values the client must not set
  4. no commit=False here

basics

~10 s

save() calls update(instance, validated_data) when the serializer was built with an instance, and create(validated_data) otherwise. Keyword arguments are merged into validated_data after validation, which is how server-side values such as created_by=request.user reach the model.

solid answer

~40 s

`save()` first asserts that `is_valid()` ran, that there are no errors, that `commit` was not passed, and that `.data` has not been read. It then builds `{**serializer.validated_data, **kwargs}` and calls `update(self.instance, validated_data)` if an instance was passed to the constructor, otherwise `create(validated_data)`; the method must return the instance, which becomes `serializer.instance`, and `serializer.data` then represents it. Keyword arguments are how the view adds values the client must not control, such as `serializer.save(created_by=self.request.user)` in `perform_create()`. They are merged after validation, so they bypass every field and `validate()` check, and they override a client value with the same key. There is no `commit=False` as in Django's `ModelForm`; to inspect data before writing, read `validated_data`.

code

python · 16 lines
python
from rest_framework import generics, permissions

from bookings.models import Booking
from bookings.serializers import BookingSerializer


class BookingListCreate(generics.ListCreateAPIView):
    serializer_class = BookingSerializer
    permission_classes = [permissions.IsAuthenticated]

    def get_queryset(self):
        return Booking.objects.filter(created_by=self.request.user)

    def perform_create(self, serializer):
        # merged into validated_data after validation, then passed to create()
        serializer.save(created_by=self.request.user)

go deeper

for a junior

Know that save() creates when no instance was passed and updates when one was, and that save(created_by=request.user) adds server-side values.

for a middle

Explain the assertions in save(), the merge of keyword arguments after validation, and what ModelSerializer's create() and update() do.

for a senior

Keep ownership and URL-derived values out of client input, validate anything passed to save() yourself, and avoid side effects before validation is complete.

for a principal

Define which values an API may accept from clients and which the server always sets, and make that split visible in serializers and views.

## The dispatch In Django REST Framework, `save()` is a short method with strict preconditions: 1. It asserts that **`is_valid()` was called**, and that **there are no errors**. 2. It asserts that no **`commit`** keyword was passed. The message explains that `commit=False` is a Django `ModelForm` idiom and that `validated_data` is the place to inspect data before writing. 3. It asserts that **`serializer.data` was not accessed** yet, since that would cache a representation of unsaved input. 4. It merges keyword arguments: `validated_data = {**self.validated_data, **kwargs}`. 5. It calls **`update(self.instance, validated_data)`** if the serializer was constructed with an instance, otherwise **`create(validated_data)`**. 6. It asserts that the method **returned an object**, stores it as `serializer.instance`, and returns it. | Serializer built as | `save()` calls | Typical view method | |---|---|---| | `BookingSerializer(data=request.data)` | `create(validated_data)` | `perform_create()` | | `BookingSerializer(booking, data=request.data)` | `update(booking, validated_data)` | `perform_update()` | | `BookingSerializer(booking, data=..., partial=True)` | `update(booking, validated_data)` | `perform_update()` via PATCH | ## Keyword arguments to save() Keyword arguments are the supported way to add values that do not come from the request body: - **Ownership**: `serializer.save(created_by=self.request.user)` in `perform_create()`. - **Values from the URL**: `serializer.save(room=room)` when the room comes from the route, not the payload. - **Server-computed values**: a status or a price calculated by the view. Three properties follow from the merge: - They are added **after validation**, so no field validator, `validate_<field>()` or `validate()` sees them. Anything that needs checking must be checked before calling `save()`. - They **override** a value with the same key from the client, which is the point when the key is `created_by`. - `ModelSerializer.create()` passes them to the model like any other key, so they must be model fields or be removed by an overridden `create()`. ## What ModelSerializer's create() and update() do - **`create()`** separates many-to-many values, calls the model's default manager `create(**validated_data)`, then sets the many-to-many relations. - **`update()`** sets an attribute for each key in `validated_data`, calls `instance.save()`, then sets many-to-many relations. - Both write **immediately**; there is no deferred flush at the end of the request. - Both refuse writable nested or dotted-source data by default; writing related objects needs an overridden method. A plain `Serializer` has no model to use, so its `create()` and `update()` raise `NotImplementedError` until you write them. **Overriding them.** When the default behaviour is not enough, override the method on the serializer and keep its contract: - Remove keys that are not model fields, such as a `coupon` code, before building the object. - Compute derived values, such as `nights` from `check_in` and `check_out`, from `validated_data`. - Call `super().create(validated_data)` when the model can still be built the default way. - **Return the instance**; `save()` asserts that something was returned. ## Where each concern belongs | Concern | Place | Reason | |---|---|---| | value from the request or URL, such as the user or the room | `perform_create()` / `perform_update()` via `save(**kwargs)` | the view owns the request | | how the object is built from data | serializer `create()` / `update()` | reusable wherever the serializer is saved | | rules about the data | `validate_<field>()` / `validate()` | errors arrive before any write | ## After save() - `serializer.instance` is the saved object. - `serializer.data` now represents that instance through `to_representation()`, so database-generated values such as the primary key appear in the response. - Calling `save()` a second time dispatches again, and because `serializer.instance` is now set, it calls `update()` with the same validated data. ## Common mistakes - Putting `created_by` in the serializer's writable fields and trusting the client to send the right user, instead of passing it to `save()`. - Calling `save(commit=False)` out of `ModelForm` habit. - Reading `serializer.data` for logging before `save()`, which trips the assertion. - Doing validation in `create()` that belongs in `validate()`, so the error arrives after other side effects have started.

  • Are values passed to DRF's serializer.save() validated?
    No. `save()` merges them into `validated_data` after `is_valid()` has finished, so no field validator, `validate_<field>()` or `validate()` sees them. That is fine for trusted server-side values like `request.user`; anything derived from user input must be validated before it is passed.
  • What happens if you call save() twice on the same DRF serializer?
    The first call creates the object and stores it as `serializer.instance`. The second call sees an instance and calls `update()` with the same `validated_data`, so it re-saves the same row rather than creating a second one, unless `serializer.data` was read in between, which makes `save()` fail its assertion.

saying these in an interview costs you the question

  • save() decides between create() and update() by checking whether the data has an id.
  • Keyword arguments to save() are validated along with the request data.
  • serializer.save(commit=False) returns an unsaved instance like a ModelForm.
  • ModelSerializer.create() defers the INSERT until the request finishes.
  • Letting the client send created_by is fine as long as the field is required.