In Django REST Framework, what does GenericAPIView.get_object() do step by step, and what breaks when a view fetches the object itself instead?
answer
- filtered, scoped queryset first
- URL kwarg versus model field
- type errors become 404
- object permissions run here
basics
~20 sget_object() filters get_queryset(), reads the lookup value from the URL kwarg, fetches with a 404-raising lookup, then runs check_object_permissions(). A hand-written Model.objects.get() skips scoping, filter backends and object permissions, and turns a missing id into a 500.
solid answer
~30 s`get_object()` starts from `self.filter_queryset(self.get_queryset())`, takes the URL keyword named by `lookup_url_kwarg` (falling back to `lookup_field`, default `'pk'`), asserts it is in `self.kwargs`, then calls DRF's `get_object_or_404`, which also maps `TypeError`, `ValueError` and `ValidationError` to 404. Finally it calls `check_object_permissions()`, which runs each permission's `has_object_permission()`. A view that does `Ticket.objects.get(pk=pk)` bypasses the scoped queryset and the permission check — so a user can reach another user's ticket by id — and a missing id raises `DoesNotExist` as a 500. Custom lookups should override `get_object()` and keep those steps.
code
python · 13 linesfrom rest_framework import generics
from .models import Ticket
from .serializers import TicketSerializer
class TicketDetail(generics.RetrieveUpdateDestroyAPIView):
serializer_class = TicketSerializer
lookup_field = "reference" # model field, e.g. "SUP-1042"
lookup_url_kwarg = "ticket_ref" # path("tickets/<str:ticket_ref>/", ...)
def get_queryset(self):
return Ticket.objects.filter(requester=self.request.user)go deeper
Know that detail views find their object through get_object(), and that lookup_field defaults to pk.
Recite the steps: filtered queryset, lookup kwarg, 404-raising lookup, object permission check. Explain lookup_field versus lookup_url_kwarg.
Diagnose an object-level leak: find the code path that fetches directly, restore get_object(), and add a second-user test for every detail route.
Treat get_object() and get_queryset() as the enforcement choke points, and require custom lookups to reuse them so authorisation cannot drift per endpoint.
## What get_object() does, in order `GenericAPIView.get_object()` in Django REST Framework (DRF) is the single place detail actions — `retrieve()`, `update()`, `partial_update()` and `destroy()` — find the object they act on. In DRF 3.18 it runs these steps: 1. **Build the base queryset**: `queryset = self.filter_queryset(self.get_queryset())`. Any scoping in `get_queryset()` and any configured filter backends apply to the lookup, not just to lists. 2. **Pick the URL keyword**: `lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field`. `lookup_field` defaults to `'pk'`; `lookup_url_kwarg` defaults to `None`, meaning "same name as the field". 3. **Assert the URL supplied it**: if that keyword is not in `self.kwargs`, an `AssertionError` says the view expected a URL keyword argument of that name — a URLconf/view mismatch, surfaced on the first request. 4. **Look it up**: `get_object_or_404(queryset, **{self.lookup_field: self.kwargs[lookup_url_kwarg]})`. 5. **Check object permissions**: `self.check_object_permissions(self.request, obj)` loops over the view's permission classes and calls `has_object_permission()` on each. 6. **Return the object.** ## The two details that make it safer than a bare get() **DRF's own `get_object_or_404`.** `rest_framework.generics` wraps Django's shortcut and also converts `TypeError`, `ValueError` and Django's `ValidationError` into `Http404`. So when the URL pattern lets `abc` through — a `<str:...>` converter or a router's default regex — `/tickets/abc/` against an integer primary key, or a malformed UUID, returns **404** rather than a server error. Django's shortcut alone catches only `DoesNotExist`; `MultipleObjectsReturned` still propagates in both, which is why `lookup_field` must name a **unique** field. **The permission hook.** When a permission denies access, `permission_denied()` raises `NotAuthenticated` if authentication was configured but did not succeed, and `PermissionDenied` (403) otherwise. This is the only place generic views run object-level permissions: `list()` does not check each row, and `create()` has no object to check. ## lookup_field versus lookup_url_kwarg | Attribute | Refers to | Default | Example | |---|---|---|---| | `lookup_field` | the **model field** filtered on | `'pk'` | `'reference'` for a ticket reference like `SUP-1042` | | `lookup_url_kwarg` | the **URL keyword** read from `self.kwargs` | `None` (falls back to `lookup_field`) | `'ticket_ref'` for `path("tickets/<str:ticket_ref>/", ...)` | Set only `lookup_field` when the URL keyword has the same name as the field. Set both when they differ. For lookups that need **several** URL keywords — say a queue slug plus a ticket number — override `get_object()` itself, and keep the permission call. With hyperlinked serializers, the serializer's URL field must be configured with the same lookup, or it will reverse URLs with the wrong keyword. ## What breaks when a view fetches the object itself A common production bug is a custom action or an overridden `retrieve()` that writes `Ticket.objects.get(pk=pk)` instead of calling `self.get_object()`: - **Scoping is bypassed.** `get_queryset()` may restrict tickets to the caller; the bare `get()` reads from all tickets. Any authenticated user can now read or modify another user's ticket by guessing its id — an insecure direct object reference. - **Object permissions are skipped.** `check_object_permissions()` is never called, so an owner-only permission class silently stops protecting that path. - **Filter backends are skipped.** Anything a filter backend enforces — for example a soft-delete filter — no longer applies. - **Errors change.** A missing id raises `DoesNotExist` (a 500) and a malformed id raises `ValueError`, instead of the 404 DRF would return. The fix is to call `self.get_object()`. If a custom lookup is unavoidable, start from `self.filter_queryset(self.get_queryset())`, use DRF's `get_object_or_404`, and call `self.check_object_permissions(self.request, obj)` before returning. ## Which actions pass through get_object() | Action | Uses `get_queryset()` | Calls `get_object()` | Object permissions | |---|---|---|---| | `list()` | yes, then filters and paginates | no | not applied per row | | `create()` | no | no | not applied — no object yet | | `retrieve()` | yes, via `get_object()` | yes | applied | | `update()` / `partial_update()` | yes, via `get_object()` | yes | applied | | `destroy()` | yes, via `get_object()` | yes | applied | The table explains why list endpoints need their visibility enforced in the queryset and why create-time rules belong in `perform_create()` or the serializer. ## Diagnosis in practice When a report says "user A could see user B's ticket through one endpoint but not the others", check: - which endpoints call `get_object()` and which fetch directly; - whether an overridden `get_object()` still calls `check_object_permissions()`; - whether `get_queryset()` is the scoped one or a view reads `self.queryset` (DRF raises on direct evaluation, but chained calls such as `self.queryset.filter(...)` bypass the override silently); - whether a test exercises every detail route with a second user. The root pattern is the same each time: the generic flow was correct, and one code path stepped around it.
- In DRF, what is the difference between lookup_field and lookup_url_kwarg on a generic view?`lookup_field` names the model field to filter on and defaults to `'pk'`. `lookup_url_kwarg` names the keyword read from the URL and defaults to `None`, meaning it reuses `lookup_field`. Set both when the URL says `ticket_ref` but the model field is `reference`; otherwise `get_object()` fails an assertion about the missing URL keyword.
- Why does /tickets/abc/ return 404 rather than 500 on a DRF detail view with an integer primary key and a string URL pattern?When the URL pattern lets `abc` through to the view, DRF's `rest_framework.generics.get_object_or_404` wraps Django's shortcut and converts `TypeError`, `ValueError` and Django's `ValidationError` from a malformed lookup value into `Http404`. Django's shortcut alone only catches `DoesNotExist`.
- Does get_object() protect against a lookup_field that is not unique?No. Neither DRF's wrapper nor Django's shortcut catches `MultipleObjectsReturned`, so two matching rows produce a server error. The lookup field must be unique in the scoped queryset, ideally enforced by a database constraint.
saying these in an interview costs you the question
- get_object() ignores get_queryset() and reads from the model's default manager.
- Generic views check object permissions on every row returned by list().
- Overriding get_object() keeps permission checks automatically without calling them.
- lookup_url_kwarg names the model field used in the filter.
- A bare Model.objects.get() in a custom action is equivalent to get_object().