In Django REST Framework, how does serializer.save() decide between create() and update(), and what do keyword arguments passed to save() do?
answer
- was an instance passed in
- merged into validated_data
- values the client must not set
- no commit=False here
basics
~10 ssave() 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 linesfrom 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
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.
Explain the assertions in save(), the merge of keyword arguments after validation, and what ModelSerializer's create() and update() do.
Keep ownership and URL-derived values out of client input, validate anything passed to save() yourself, and avoid side effects before validation is complete.
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.