Skip to main content

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)

Source distribution for drf-choices-mixin 0.1.3
File Size Uploaded Provenance
drf_choices_mixin-0.1.3.tar.gz 59.2 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for drf-choices-mixin 0.1.3
File Interpreter ABI Platform
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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page