Skip to main content

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 typesTextChoices, 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 choicesGET /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 fieldGET /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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

drf_choices_mixin-0.1.2.tar.gz (59.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

drf_choices_mixin-0.1.2-py3-none-any.whl (6.3 kB view details)

Uploaded Python 3

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

Hashes for drf_choices_mixin-0.1.2.tar.gz
Algorithm Hash digest
SHA256 9385a96fa5144ea3047ba2505976e6042946c6aae4ed4e8f553a9b4cf0260692
MD5 29eb7d1eab843b43bb81e1cb0e138dfb
BLAKE2b-256 0c609802189d1d7dd6b0da85e64e69583089d51e96827c2dd3724a75fc363472

See more details on using hashes here.

Provenance

The following attestation bundles were made for drf_choices_mixin-0.1.2.tar.gz:

Publisher: ci.yml on dennybiasiolli/drf-choices-mixin

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file drf_choices_mixin-0.1.2-py3-none-any.whl.

File metadata

File hashes

Hashes for drf_choices_mixin-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 5b52097392999af5c21d1cb817972d4f2b5bf0759be539658941ac3f9b08257d
MD5 05ecdeffc5026792358f95390fc1cdc8
BLAKE2b-256 ba1b3356e185135211c473dc7a509d73e81c1053b18b8dda7f7d2cae1ec0792e

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page