Django: detail view must be called with pk or slug

Learn how to use UUID as URLs in your Django projects.

Welcome back to another episode of my Django mini-tutorials!

Lately I've been experimenting with UUID as public identifiers in my Django URLs, an approach suggested in Two Scoops of Django, which incidentally I recall also having read from REST in practice, an old book from 2010.

This technique consists of URLs made out from opaque identifiers, such as random numbers, or better, UUID. The goal is to obscure the model's primary key in your URLs.

Opaque URLs in Django

Let's see opaque URLs in practice.

First off, in the templates you build your links as follows:

// IMAGINE A FOR LOOP!
<a href="{% url "ticket-detail" ticket.uuid %}">{{ ticket.subject }}</a>
<a href="{% url "ticket-detail" ticket.uuid %}">{{ ticket.subject }}</a>

This template can be served from a ListView for example, to render a list of models.

Here ticket-detail is a named Django view, configured in URLconf as follows:

urlpatterns = [
    path(
        "tickets/<uuid:uuid>/",
        TicketDetail.as_view(),
        name="ticket-detail",
    ),
]

As a path for the view we accept the uuid argument.

The uuid field must be present in the model:

class Ticket(models.Model):
    uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False)
    # Other fields ...

This makes possible to serve URLs like /tickets/a3c99176-31e8-4e69-87f3-122f2fe4022f/ which might not be the friendliest URL ever, but it helps to obscure the model's primary key in your URLs.

UUID and Django's DetailView

To render a single model when the user clicks on a link like /tickets/a3c99176-31e8-4e69-87f3-122f2fe4022f/ we can use a DetailView, which conveniently takes just the model as an attribute:

class TicketDetail(DetailView):
    model = Ticket

The problem here is that DetailView doesn't absolutely know how to fetch the right object from the database based on the UUID passed in the URL.

In fact, if we try to visit something like /tickets/a3c99176-31e8-4e69-87f3-122f2fe4022f/, Django can't do anything than scream back:

Generic detail view must be called with either an object_pk or a slug in the URLConf

This exception is raised by get_object() in SingleObjectMixin which expects either slug or pk as arguments for the descendant generic view.

PROTIP: It's likely that this functionality is required in more than one view of your project. To keep things clean, you can make your own mixin SingleObjectSlugMixin by subclassing SingleObjectMixin.

To make DetailView happy again we can override get_object() so that it gets the desired object from the database, starting from the uuid:

class TicketDetail(DetailView):
    model = Ticket

    def get_object(self, queryset=None):
        return Ticket.objects.get(uuid=self.kwargs.get("uuid"))

Thanks for reading!

Valentino Gagliardi

Hi! I'm Valentino! I'm a freelance consultant with a wealth of experience in the IT industry. I spent the last years as a frontend consultant, providing advice and help, coaching and training on JavaScript, testing, and software development. Let's get in touch!

More from the blog: