skip to content

In a Django REST Framework ListCreateAPIView for support tickets, how do you set the requester from request.user, and why override perform_create()?

level: middleimportance: must knowfreq 64%

answer

  1. value implied by the request
  2. smallest hook in create()
  3. save() takes extra kwargs
  4. validation and 201 stay intact

basics

~10 s

Override 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 lines
python
from 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

for a junior

Remember the pattern serializer.save(requester=self.request.user) inside perform_create(), and that the requester field should be read-only.

for a middle

Walk through CreateModelMixin.create(): validate with raise_exception, call perform_create(), build headers, return 201. Explain how save() merges keyword arguments into validated_data.

for a senior

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.

for a principal

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.