In a Django REST Framework ListCreateAPIView for support tickets, how do you set the requester from request.user, and why override perform_create()?
answer
- value implied by the request
- smallest hook in create()
- save() takes extra kwargs
- validation and 201 stay intact
basics
~10 sOverride perform_create(self, serializer) and call serializer.save(requester=self.request.user). CreateModelMixin.create() still validates input, calls the hook, and returns 201, so only the save step changes and none of the response plumbing is copied.
solid answer
~30 s`ListCreateAPIView.post()` calls `CreateModelMixin.create()`, which validates the serializer with `raise_exception=True`, calls `perform_create(serializer)`, then returns 201 with an optional `Location` header. The default `perform_create()` is just `serializer.save()`, so I override it with `serializer.save(requester=self.request.user)`; keyword arguments are merged into `validated_data` and win over input. The `requester` field is read-only on the serializer, so clients cannot set it. Overriding `create()` instead would mean re-implementing validation, status and headers. The same hook is where create-time rules go — raising `ValidationError` or `PermissionDenied` — because object-level permissions never run on create: `create()` does not call `get_object()`.
code
python · 23 linesfrom rest_framework import generics, permissions
from rest_framework.exceptions import ValidationError
from .models import Ticket
from .serializers import TicketSerializer
MAX_OPEN_TICKETS = 5
class TicketListCreate(generics.ListCreateAPIView):
serializer_class = TicketSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Ticket.objects.filter(requester=self.request.user)
def perform_create(self, serializer):
open_count = Ticket.objects.filter(
requester=self.request.user, status=Ticket.Status.OPEN
).count()
if open_count >= MAX_OPEN_TICKETS:
raise ValidationError("You already have too many open tickets.")
serializer.save(requester=self.request.user)go deeper
Remember the pattern serializer.save(requester=self.request.user) inside perform_create(), and that the requester field should be read-only.
Walk through CreateModelMixin.create(): validate with raise_exception, call perform_create(), build headers, return 201. Explain how save() merges keyword arguments into validated_data.
Use the hook for create-time rules and explain why object permissions never guard creation. Flag overrides of create() in review as a source of wrong status codes.
Set a convention: ownership and tenancy are stamped in view hooks from authenticated identity, never from payload, and serializers stay reusable across admin and API paths.
## The requirement A support-ticket API has a collection endpoint, `/tickets/`, built on Django REST Framework's (DRF) `ListCreateAPIView`. When a signed-in user posts a new ticket, its `requester` foreign key must be the caller — never a value the client chose. The `requester` value is **implicit in the request**, not part of the request body, so the serializer cannot validate it from input. ## Where the hook sits in CreateModelMixin.create() `ListCreateAPIView.post()` calls `CreateModelMixin.create()`, which runs a fixed sequence: 1. `serializer = self.get_serializer(data=request.data)` — builds the serializer with the view's context. 2. `serializer.is_valid(raise_exception=True)` — invalid input raises `ValidationError`, which becomes a **400** response. 3. `self.perform_create(serializer)` — the **save hook**; its default body is just `serializer.save()`. 4. `headers = self.get_success_headers(serializer.data)` — adds `Location` when the representation has a `url` key. 5. `return Response(serializer.data, status=201, headers=headers)`. Only step 3 needs to change. Override it and pass the extra attribute to `save()`: - `serializer.save(requester=self.request.user)` merges keyword arguments into `validated_data` — DRF builds `{**validated_data, **kwargs}` — before calling the serializer's `create()`, so the keyword wins even if the same key came from input. - Mark the field read-only on the serializer so clients do not see it as writable input; how you declare that belongs to serializer design. ## Why not override create()? | Override | What you must keep correct yourself | Typical bug | |---|---|---| | `perform_create()` | Only the save call | Forgetting to call `serializer.save()` at all | | `create()` | Validation, the 201 status, the `Location` header, the response body | Returning 200, dropping `raise_exception=True`, losing headers | | `post()` | Everything above, plus the action binding | Bypassing the mixin entirely and diverging from other endpoints | Overriding the smallest hook keeps the rest of the generic flow — and any future DRF fix to it — intact. The sibling hooks follow the same pattern: `perform_update(serializer)` in `UpdateModelMixin` and `perform_destroy(instance)` in `DestroyModelMixin`. ## Other legitimate uses of the save hooks - **Values from the URL**: a nested endpoint such as `/queues/<queue_id>/tickets/` can pass `queue_id=self.kwargs["queue_id"]` to `save()`. - **Create-time rules**: raise `rest_framework.exceptions.ValidationError` (400) or `PermissionDenied` (403) before saving — for example, refusing a new ticket while the caller already has too many open ones. This matters because **object-level permissions do not run on create**: `has_object_permission()` is invoked from `get_object()`, which `create()` never calls. The DRF documentation names the serializer or `perform_create()` as the place for such checks. - **Side effects after the save**: `instance = serializer.save(...)`, then enqueue a notification or write an audit row. Keep the side effect robust to a later rollback; how you tie it to the transaction is a separate topic. ## Common mistakes - Writing `serializer.save(commit=False)` out of Django `ModelForm` habit. DRF's `save()` asserts that `commit` is not a keyword argument and fails with a message pointing you at `serializer.validated_data`. - Reading `request.data["requester"]` and trusting it. The caller's identity comes from `self.request.user`, which authentication set; body data is client-controlled. - Burying ownership in the serializer through `self.context["request"]`. It works, because the view's `get_serializer()` passes `request` in the context, but it ties the serializer to HTTP requests; stamping ownership in the view keeps the serializer reusable for admin tools, scripts and tests. - Overriding `create()` and calling `serializer.save()` twice — once in the override and once through the inherited `perform_create()`. ## Testing the hook A test should prove the rule, not the happy path. With DRF's `APIClient` from `rest_framework.test`: 1. Create two users, A and B, and call `client.force_authenticate(user=a)`. 2. POST a ticket body that also claims `"requester": b.pk`. 3. Assert the response is 201 and that the saved ticket's `requester` is A, not B. 4. For the create-time rule, pre-create the maximum number of open tickets for A, POST again, and assert a 400 with no new row. Such a test catches both regressions that matter in review: a field accidentally made writable, and an override that stops calling the hook. ## Summary `perform_create()` is the extension point DRF provides for exactly this case: the input is validated, you add what the request implies, and the mixin still produces the correct 201 response. Reach for `create()` only when the response itself must change.
- If the client also sends a requester value and the field is writable, which value does DRF save?The keyword argument. `Serializer.save(**kwargs)` builds `{**validated_data, **kwargs}`, so `requester=self.request.user` overrides whatever was validated from input. The field should still be read-only, both so the API schema is honest and so a future refactor that drops the keyword cannot silently let clients pick the owner.
- Why does a has_object_permission() check not protect ticket creation in a DRF generic view?Object permissions run inside `get_object()`, via `check_object_permissions()`. `CreateModelMixin.create()` never calls `get_object()` — there is no existing object yet — so only view-level `has_permission()` applies. Create-time rules must live in `perform_create()` or in serializer validation.
- What happens if you call serializer.save(commit=False) in perform_create()?It fails with an assertion: DRF's `save()` explicitly rejects a `commit` keyword and tells you to inspect `serializer.validated_data` instead. `commit=False` is a Django `ModelForm` idiom; in DRF you pass extra attributes to `save()` or read `validated_data` before saving.
A clerk stamps the date and the customer's name on a form the customer filled in: the customer never writes those boxes, and the clerk does not re-check the rest of the form.
saying these in an interview costs you the question
- Read the requester id from request.data and trust it.
- Override create() and copy its body to add one attribute.
- Use serializer.save(commit=False) and then set requester on the instance.
- has_object_permission() already guards who may create a ticket.
- perform_create() runs before the serializer validates the input.