Stripe-like prefixed IDs for Django. Works with existing UUIDs — no schema changes.
Project description
django-display-ids
Stripe-like prefixed IDs for Django. Works with existing UUIDs — no schema changes.
Display IDs are human-friendly identifiers like inv_2aUyqjCzEIiEcYMKj7TZtw — a short prefix indicating the object type, followed by a base62-encoded UUID. This format, popularized by Stripe, makes IDs recognizable at a glance while remaining URL-safe and compact.
This library focuses on lookup only — it works with your existing UUID fields and requires no migrations or schema changes.
Installation
pip install django-display-ids
No INSTALLED_APPS entry required — just import and use.
Quick Start
from django.views.generic import DetailView
from django_display_ids import DisplayIDObjectMixin
class InvoiceDetailView(DisplayIDObjectMixin, DetailView):
model = Invoice
lookup_param = "id"
lookup_strategies = ("display_id", "uuid")
display_id_prefix = "inv"
# urls.py
urlpatterns = [
path("invoices/<str:id>/", InvoiceDetailView.as_view()),
]
Now your view accepts both formats:
inv_2aUyqjCzEIiEcYMKj7TZtw(display ID)550e8400-e29b-41d4-a716-446655440000(UUID)
Features
- Multiple identifier formats: display ID (
prefix_base62uuid), UUID (v4/v7), slug - Framework support: Django CBVs and Django REST Framework
- Zero model changes required: Works with any existing UUID field
- Stateless: Pure lookup, no database writes
Usage
Django Class-Based Views
from django.views.generic import DetailView, UpdateView, DeleteView
from django_display_ids import DisplayIDObjectMixin
class InvoiceDetailView(DisplayIDObjectMixin, DetailView):
model = Invoice
lookup_param = "id"
lookup_strategies = ("display_id", "uuid")
display_id_prefix = "inv"
# Works with any view that uses get_object()
class InvoiceUpdateView(DisplayIDObjectMixin, UpdateView):
model = Invoice
lookup_param = "id"
display_id_prefix = "inv"
Django REST Framework
from rest_framework.viewsets import ModelViewSet
from django_display_ids.contrib.rest_framework import DisplayIDLookupMixin
class InvoiceViewSet(DisplayIDLookupMixin, ModelViewSet):
queryset = Invoice.objects.all()
serializer_class = InvoiceSerializer
lookup_url_kwarg = "id"
lookup_strategies = ("display_id", "uuid")
display_id_prefix = "inv"
Or with APIView:
from rest_framework.views import APIView
from rest_framework.response import Response
from django_display_ids.contrib.rest_framework import DisplayIDLookupMixin
class InvoiceView(DisplayIDLookupMixin, APIView):
lookup_url_kwarg = "id"
lookup_strategies = ("display_id", "uuid")
display_id_prefix = "inv"
def get_queryset(self):
return Invoice.objects.all()
def get(self, request, *args, **kwargs):
invoice = self.get_object()
return Response({"id": str(invoice.id)})
Serializer Field
Include display_id in your API responses:
from rest_framework import serializers
from django_display_ids.contrib.rest_framework import DisplayIDField
class InvoiceSerializer(serializers.Serializer):
id = serializers.UUIDField(read_only=True)
display_id = DisplayIDField()
name = serializers.CharField()
# Output: {"id": "...", "display_id": "inv_2aUyqjCzEIiEcYMKj7TZtw", ...}
The field reads display_id_prefix from the model. You can override it:
display_id = DisplayIDField(prefix="inv") # Use custom prefix
Prefix must be 1-16 lowercase letters. Invalid prefixes raise ValueError at initialization.
OpenAPI/drf-spectacular: When drf-spectacular is installed, the field automatically generates proper schema with prefix-specific examples (e.g., inv_2aUyqjCzEIiEcYMKj7TZtw). The prefix is resolved from (in order): field's prefix= argument, serializer's Meta.model.display_id_prefix, or the view's queryset model.
OpenAPI Parameter Descriptions
For consistent API documentation, use the provided description helpers:
from django_display_ids.contrib.rest_framework import id_param_description
from drf_spectacular.utils import extend_schema, OpenApiParameter
from drf_spectacular.types import OpenApiTypes
@extend_schema(
parameters=[
OpenApiParameter(
"id",
OpenApiTypes.STR,
OpenApiParameter.PATH,
description=id_param_description("inv"),
# -> "Identifier: display_id (inv_xxx) or UUID"
)
],
)
class InvoiceViewSet(DisplayIDLookupMixin, ModelViewSet):
...
For endpoints that also accept slugs:
description=id_param_description("app", with_slug=True)
# -> "Identifier: display_id (app_xxx), UUID, or slug"
Generic constants are also available:
from django_display_ids.contrib.rest_framework import (
ID_PARAM_DESCRIPTION, # "Identifier: display_id (prefix_xxx) or UUID"
ID_PARAM_DESCRIPTION_WITH_SLUG, # "Identifier: display_id (prefix_xxx), UUID, or slug"
)
Deterministic Examples for OpenAPI
Generate consistent example UUIDs and display IDs for OpenAPI schemas:
from django_display_ids import example_uuid, example_display_id
# Generate deterministic UUID for a prefix
example_uuid("inv")
# -> UUID('a172cedc-ae47-474b-615c-54d510a5d84a')
# Generate deterministic display ID
example_display_id("inv")
# -> "inv_4ueEO5Nz4X7u9qc3FVHokM"
# Also works with model classes
example_uuid(Invoice) # Uses Invoice.display_id_prefix
The same prefix always produces the same example, ensuring consistent documentation across regenerations.
Model Mixin
Add a display_id property to your models:
import uuid
from django.db import models
from django_display_ids import DisplayIDMixin
class Invoice(DisplayIDMixin, models.Model):
display_id_prefix = "inv"
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
invoice = Invoice.objects.first()
invoice.display_id # -> "inv_2aUyqjCzEIiEcYMKj7TZtw"
Model Manager
from django_display_ids import DisplayIDMixin, DisplayIDManager
class Invoice(DisplayIDMixin, models.Model):
display_id_prefix = "inv"
objects = DisplayIDManager()
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
# Get by display ID
invoice = Invoice.objects.get_by_display_id("inv_2aUyqjCzEIiEcYMKj7TZtw")
# Get by any identifier type
invoice = Invoice.objects.get_by_identifier("inv_2aUyqjCzEIiEcYMKj7TZtw")
invoice = Invoice.objects.get_by_identifier("550e8400-e29b-41d4-a716-446655440000")
# Works with filtered querysets
invoice = Invoice.objects.filter(active=True).get_by_identifier("inv_xxx")
Django Admin
Enable searching by display ID or raw UUID in the admin:
from django.contrib import admin
from django_display_ids import DisplayIDSearchMixin
@admin.register(Invoice)
class InvoiceAdmin(DisplayIDSearchMixin, admin.ModelAdmin):
list_display = ["id", "display_id", "name", "created"]
search_fields = ["name"] # display_id/UUID search is automatic
Now you can search by either format in the admin search box:
inv_2aUyqjCzEIiEcYMKj7TZtw(display ID)550e8400-e29b-41d4-a716-446655440000(raw UUID from logs)
The mixin automatically detects the UUID field from your model's uuid_field
attribute (if using DisplayIDMixin), or defaults to id. Override with:
class InvoiceAdmin(DisplayIDSearchMixin, admin.ModelAdmin):
uuid_field = "uid" # custom UUID field name
Encoding and Decoding
import uuid
from django_display_ids import encode_display_id, decode_display_id
# Create a display ID from a UUID
invoice_id = uuid.uuid4()
display_id = encode_display_id("inv", invoice_id)
# -> "inv_2aUyqjCzEIiEcYMKj7TZtw"
# Decode back to prefix and UUID
prefix, decoded_uuid = decode_display_id(display_id)
Direct Resolution
from django_display_ids import resolve_object
invoice = resolve_object(
model=Invoice,
value="inv_2aUyqjCzEIiEcYMKj7TZtw",
strategies=("display_id", "uuid", "slug"),
prefix="inv",
)
Identifier Formats
| Format | Example | Description |
|---|---|---|
| Display ID | inv_2aUyqjCzEIiEcYMKj7TZtw |
Prefix + base62-encoded UUID |
| UUID | 550e8400-e29b-41d4-a716-446655440000 |
Standard UUID (v4/v7) |
| Slug | my-invoice-slug |
Human-readable identifier |
Display ID format:
- Prefix: 1-16 lowercase letters
- Separator: underscore
- Encoded UUID: 22 base62 characters (fixed length)
Lookup Strategies
Strategies are tried in order. The first successful match is returned.
| Strategy | Description |
|---|---|
display_id |
Decode display ID, lookup by UUID field |
uuid |
Parse as UUID, lookup by UUID field |
slug |
Lookup by slug field |
Default: ("display_id", "uuid")
The slug strategy is a catch-all, so it should always be last.
The display_id strategy requires a prefix. If no prefix is configured, the strategy is skipped.
Configuration
View/Mixin Attributes
| Attribute | Default | Description |
|---|---|---|
lookup_param / lookup_url_kwarg |
"pk" |
URL parameter name |
lookup_strategies |
from settings | Strategies to try |
display_id_prefix |
from model | Expected prefix (falls back to model's display_id_prefix) |
uuid_field |
"id" |
UUID field name on model |
slug_field |
"slug" |
Slug field name on model |
Django Settings (Optional)
All settings have sensible defaults. Only add this if you need to override them:
# settings.py
DISPLAY_IDS = {
"UUID_FIELD": "id", # default
"SLUG_FIELD": "slug", # default
"STRATEGIES": ("display_id", "uuid"), # default
}
Error Handling
| Exception | When Raised |
|---|---|
InvalidIdentifierError |
Identifier cannot be parsed |
UnknownPrefixError |
Display ID prefix doesn't match expected |
ObjectNotFoundError |
No matching database record |
In views, errors are converted to HTTP responses:
- Django CBV:
Http404 - DRF:
NotFound(404) orParseError(400)
Requirements
- Python 3.12+
- Django 4.2+
- Django REST Framework 3.14+ (optional)
Development
Clone the repository and install dependencies:
git clone https://github.com/josephabrahams/django-display-ids.git
cd django-display-ids
uv sync
Run tests:
uv run pytest
Run tests with coverage:
uv run pytest --cov=src/django_display_ids
Run tests across Python and Django versions:
uvx nox
Lint and format:
uvx pre-commit run --all-files
Related Projects
If you need ID generation and storage (custom model fields), consider these alternatives:
- django-prefix-id — PrefixIDField that generates and stores base62-encoded UUIDs
- django-spicy-id — Drop-in AutoField replacement that displays numeric IDs as prefixed strings
- django-charid-field — CharField wrapper supporting cuid, ksuid, ulid, and other generators
django-display-ids takes a different approach: it works with your existing UUID fields and handles resolution only. No migrations, no schema changes — just add the mixin to your views.
License
ISC
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_display_ids-0.1.4.tar.gz.
File metadata
- Download URL: django_display_ids-0.1.4.tar.gz
- Upload date:
- Size: 17.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
152e1fff3e7cdd04b7eaee9b26582eb5b155eda2312187d355c4613e4379f0a6
|
|
| MD5 |
1dac6f7dfd3fbdc0dbcda53390c2596c
|
|
| BLAKE2b-256 |
2db1b33cb6cb0167113cd13c751c0f4cbdecfb7b1c064eebe2041aa760bd475d
|
Provenance
The following attestation bundles were made for django_display_ids-0.1.4.tar.gz:
Publisher:
ci.yml on josephabrahams/django-display-ids
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_display_ids-0.1.4.tar.gz -
Subject digest:
152e1fff3e7cdd04b7eaee9b26582eb5b155eda2312187d355c4613e4379f0a6 - Sigstore transparency entry: 855084626
- Sigstore integration time:
-
Permalink:
josephabrahams/django-display-ids@2601e6739d8baea7b2cf3e6c539004237a395041 -
Branch / Tag:
refs/tags/0.1.4 - Owner: https://github.com/josephabrahams
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@2601e6739d8baea7b2cf3e6c539004237a395041 -
Trigger Event:
push
-
Statement type:
File details
Details for the file django_display_ids-0.1.4-py3-none-any.whl.
File metadata
- Download URL: django_display_ids-0.1.4-py3-none-any.whl
- Upload date:
- Size: 27.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
88748265e6c70a7d5b2d5d6e2ccfbcbaa3306d68c0af5947515d1455913cefea
|
|
| MD5 |
0b366a6acf14a0876690c0ffcdbb7078
|
|
| BLAKE2b-256 |
c68fd52b6a56da84e1a2f3c7186652f4e06f200c75352c35eafd6df80410f76b
|
Provenance
The following attestation bundles were made for django_display_ids-0.1.4-py3-none-any.whl:
Publisher:
ci.yml on josephabrahams/django-display-ids
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_display_ids-0.1.4-py3-none-any.whl -
Subject digest:
88748265e6c70a7d5b2d5d6e2ccfbcbaa3306d68c0af5947515d1455913cefea - Sigstore transparency entry: 855084628
- Sigstore integration time:
-
Permalink:
josephabrahams/django-display-ids@2601e6739d8baea7b2cf3e6c539004237a395041 -
Branch / Tag:
refs/tags/0.1.4 - Owner: https://github.com/josephabrahams
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@2601e6739d8baea7b2cf3e6c539004237a395041 -
Trigger Event:
push
-
Statement type: