drf-choices-mixin
A Django REST Framework mixin that automatically generates API endpoints for
model field choices. Drop ChoicesMixin into any DRF viewset and it will
expose every field that defines choices through list-level endpoints — no
manual serializer or view code required.
Features
- Zero configuration — add the mixin and endpoints appear automatically.
- Fully configurable — customize endpoint names, URL structure, exposed fields, and response format.
- Works with all choice types —
TextChoices,IntegerChoices, plain tuples, and grouped choices. - Two endpoints per viewset — one for all choices, one for a specific field.
Installation
pip install drf-choices-mixin
Quick start
Given a model with choices:
from django.db import models
class Status(models.TextChoices):
ACTIVE = "active", "Active"
INACTIVE = "inactive", "Inactive"
class Priority(models.IntegerChoices):
LOW = 1, "Low"
MEDIUM = 2, "Medium"
HIGH = 3, "High"
class Task(models.Model):
title = models.CharField(max_length=100)
status = models.CharField(max_length=20, choices=Status.choices)
priority = models.IntegerField(choices=Priority.choices)
description = models.TextField()
Add the mixin to your viewset (place it before the base viewset class):
from drf_choices_mixin import ChoicesMixin
from rest_framework.viewsets import ModelViewSet
class TaskViewSet(ChoicesMixin, ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
Register the viewset with a router as usual:
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register("tasks", TaskViewSet)
Two new endpoints are now available:
| Endpoint | Description |
|---|---|
GET /tasks/choices/ |
All choices for every field |
GET /tasks/choices/{field_name}/ |
Choices for a specific field |
Response examples
All choices — GET /tasks/choices/
{
"status": [
{"value": "active", "display": "Active"},
{"value": "inactive", "display": "Inactive"}
],
"priority": [
{"value": 1, "display": "Low"},
{"value": 2, "display": "Medium"},
{"value": 3, "display": "High"}
]
}
Single field — GET /tasks/choices/status/
[
{"value": "active", "display": "Active"},
{"value": "inactive", "display": "Inactive"}
]
Requesting a field that has no choices or does not exist returns a 404:
{"detail": "Field 'title' does not have choices."}
Configuration
All configuration is done through class attributes on the viewset. Every option has a sensible default, so none of them are required.
| Attribute | Type | Default | Description |
|---|---|---|---|
choices_endpoint_name |
str |
"choices" |
URL segment used for both endpoints |
choices_field_first |
bool |
False |
Put the field name before the endpoint segment in URLs |
choices_fields |
list[str] | None |
None |
Restrict which fields are exposed (None = all) |
choices_value_key |
str |
"value" |
Key name for the choice value in the response |
choices_display_key |
str |
"display" |
Key name for the display text in the response |
Custom endpoint name
Rename the URL segment to avoid conflicts with other actions or to match your API conventions:
class TaskViewSet(ChoicesMixin, ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
choices_endpoint_name = "options"
This changes the endpoints to:
| Before | After |
|---|---|
GET /tasks/choices/ |
GET /tasks/options/ |
GET /tasks/choices/status/ |
GET /tasks/options/status/ |
URL order — field-first mode
By default the endpoint name comes first (/choices/status/). Set
choices_field_first = True to invert the order, which can read more
naturally in some APIs:
class TaskViewSet(ChoicesMixin, ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
choices_field_first = True
| Default | Field-first |
|---|---|
GET /tasks/choices/status/ |
GET /tasks/status/choices/ |
The all-choices endpoint stays the same (GET /tasks/choices/).
Both options can be combined:
class TaskViewSet(ChoicesMixin, ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
choices_endpoint_name = "options"
choices_field_first = True
| Endpoint | Description |
|---|---|
GET /tasks/options/ |
All choices |
GET /tasks/status/options/ |
Choices for the status field |
Field filtering
By default every field with choices is exposed. Use choices_fields to
restrict the output to a specific subset:
class TaskViewSet(ChoicesMixin, ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
choices_fields = ["status"]
GET /tasks/choices/ now returns only the status field:
{
"status": [
{"value": "active", "display": "Active"},
{"value": "inactive", "display": "Inactive"}
]
}
Fields not in the list return 404 when accessed directly
(GET /tasks/choices/priority/ → 404).
Custom response keys
If your frontend expects different key names, override choices_value_key
and choices_display_key:
class TaskViewSet(ChoicesMixin, ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
choices_value_key = "key"
choices_display_key = "label"
GET /tasks/choices/status/ now returns:
[
{"key": "active", "label": "Active"},
{"key": "inactive", "label": "Inactive"}
]
Full example
All options together:
class TaskViewSet(ChoicesMixin, ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
choices_endpoint_name = "options"
choices_field_first = True
choices_fields = ["status", "priority"]
choices_value_key = "id"
choices_display_key = "text"
| Endpoint | Response |
|---|---|
GET /tasks/options/ |
{"status": [{"id": "active", "text": "Active"}, ...], "priority": [...]} |
GET /tasks/status/options/ |
[{"id": "active", "text": "Active"}, ...] |
GET /tasks/description/options/ |
404 |
Supported choice formats
The mixin works with every way Django lets you define choices:
TextChoices / IntegerChoices enums (Django 3.0+):
class Status(models.TextChoices):
ACTIVE = "active", "Active"
INACTIVE = "inactive", "Inactive"
status = models.CharField(choices=Status.choices)
Plain tuples:
status = models.CharField(choices=[("active", "Active"), ("inactive", "Inactive")])
Grouped choices (automatically flattened):
COLOR_CHOICES = [
("Warm", [("red", "Red"), ("orange", "Orange")]),
("Cool", [("blue", "Blue"), ("green", "Green")]),
]
color = models.CharField(choices=COLOR_CHOICES)
Response for grouped choices:
[
{"value": "red", "display": "Red"},
{"value": "orange", "display": "Orange"},
{"value": "blue", "display": "Blue"},
{"value": "green", "display": "Green"}
]
Development
# Install dev dependencies
uv sync --group dev
# Run tests
uv run pytest
# Lint & format
uv run ruff check .
uv run ruff format .
# Build documentation locally
uv sync --group docs
make docs
Credits
This project was built with the assistance of Grok, an AI model by xAI.
License
MIT
Metadata
Release files for drf-choices-mixin 0.1.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| drf_choices_mixin-0.1.3.tar.gz | 59.2 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| drf_choices_mixin-0.1.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 65.6 kB
Release files / drf_choices_mixin-0.1.3.tar.gz
| Download URL | drf_choices_mixin-0.1.3.tar.gz |
|---|---|
| Size | 59.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e632a8689115a09460d6aa32d81506a5e346740e33ab4ca4db35d521bee2ac8b
|
|
BLAKE2b-256 checksum How to use checksums |
75c6d9436bfccd497d54ecd7c6c46c9588c880047ef0dda836ba3327a5263748
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / drf_choices_mixin-0.1.3-py3-none-any.whl
| Download URL | drf_choices_mixin-0.1.3-py3-none-any.whl |
|---|---|
| Size | 6.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d1d21d9c961a9d63b40a31fbec116d5ea999e4156d984657893041f61bc498df
|
|
BLAKE2b-256 checksum How to use checksums |
86a8289a31b842372a8f7292f0fd3b3d513f462b1d6c621b0935eb5bb6200fe2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for drf-choices-mixin 0.1.3
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.