A Django REST Framework mixin that auto-generates API endpoints for model field choices
Project description
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
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 drf_choices_mixin-0.1.2.tar.gz.
File metadata
- Download URL: drf_choices_mixin-0.1.2.tar.gz
- Upload date:
- Size: 59.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9385a96fa5144ea3047ba2505976e6042946c6aae4ed4e8f553a9b4cf0260692
|
|
| MD5 |
29eb7d1eab843b43bb81e1cb0e138dfb
|
|
| BLAKE2b-256 |
0c609802189d1d7dd6b0da85e64e69583089d51e96827c2dd3724a75fc363472
|
Provenance
The following attestation bundles were made for drf_choices_mixin-0.1.2.tar.gz:
Publisher:
ci.yml on dennybiasiolli/drf-choices-mixin
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
drf_choices_mixin-0.1.2.tar.gz -
Subject digest:
9385a96fa5144ea3047ba2505976e6042946c6aae4ed4e8f553a9b4cf0260692 - Sigstore transparency entry: 1908366651
- Sigstore integration time:
-
Permalink:
dennybiasiolli/drf-choices-mixin@a3aded90b2e887e96fb72819517a5fbe19680eb0 -
Branch / Tag:
- Owner: https://github.com/dennybiasiolli
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@a3aded90b2e887e96fb72819517a5fbe19680eb0 -
Trigger Event:
release
-
Statement type:
File details
Details for the file drf_choices_mixin-0.1.2-py3-none-any.whl.
File metadata
- Download URL: drf_choices_mixin-0.1.2-py3-none-any.whl
- Upload date:
- Size: 6.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5b52097392999af5c21d1cb817972d4f2b5bf0759be539658941ac3f9b08257d
|
|
| MD5 |
05ecdeffc5026792358f95390fc1cdc8
|
|
| BLAKE2b-256 |
ba1b3356e185135211c473dc7a509d73e81c1053b18b8dda7f7d2cae1ec0792e
|
Provenance
The following attestation bundles were made for drf_choices_mixin-0.1.2-py3-none-any.whl:
Publisher:
ci.yml on dennybiasiolli/drf-choices-mixin
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
drf_choices_mixin-0.1.2-py3-none-any.whl -
Subject digest:
5b52097392999af5c21d1cb817972d4f2b5bf0759be539658941ac3f9b08257d - Sigstore transparency entry: 1908366811
- Sigstore integration time:
-
Permalink:
dennybiasiolli/drf-choices-mixin@a3aded90b2e887e96fb72819517a5fbe19680eb0 -
Branch / Tag:
- Owner: https://github.com/dennybiasiolli
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@a3aded90b2e887e96fb72819517a5fbe19680eb0 -
Trigger Event:
release
-
Statement type: