django-ninja API for IntegerChoices

Meta: This article was written using the STAR method.

Situation

We were developing an API using Django Ninja which relies on Djangos IntegerChoices like this:

# models.py

class GuetestufeWahl(models.IntegerChoices):
    """
    Gütestufeskala gemäss Mini-ICF-APP Work.
    """

    KEINE_BEEINTRAECHTIGUNG = 0, _("0: Keine Beeinträchtigung")
    LEICHTE_BEEINTRAECHTIGUNG = 1, _("1: Leichte Beeinträchtigung (ohne Negativfolgen)")
    MAESSIGE_BEEINTRAECHTIGUNG = 2, _("2: Mässige Beeinträchtigung (Negativfolgen selbst behebbar)")
    ERHEBLICHE_BEEINTRAECHTIGUNG = 3, _("3: Erhebliche Beeinträchtigung (Assistenz für Folgenbehebung nötig)")
    VOLLSTAENDIGE_BEEINTRAECHTIGUNG = 4, _("4: Vollständige Beeinträchtigung (Assistenz immer nötig)")

Task

Make sure the generated Typescript interfaces are properly named, matching the Django model definition. This ensures type safety on the frontend and maintains consistency between the code bases of backend and the frontend.

Action

We came up with a mixin-class, that makes use of x-enum-varnames and x-enum-descriptions to make sure, the API will be generated accordingly.

# utils/mixins.py

from pydantic import GetJsonSchemaHandler
from pydantic.json_schema import JsonSchemaValue


class OpenApiEnumMixin:
    """
    Mixin to inject x-enum-varnames into the OpenAPI schema for Django Choices.
    """

    @classmethod
    def __get_pydantic_json_schema__(cls, core_schema: dict, handler: GetJsonSchemaHandler) -> JsonSchemaValue:
        # Generate the default schema (type: integer, enum: [0, 1, ...])
        json_schema = handler(core_schema)

        # Inject the custom extensions for openapi-generator
        json_schema["x-enum-varnames"] = [member.name for member in cls]
        json_schema["x-enum-descriptions"] = [str(member.label) for member in cls]

        return json_schema

Result

Finally, just inherit from the mixin-class like this:

from .utils import mixins

class GuetestufeWahl(mixins.OpenApiEnumMixin, models.IntegerChoices):
    ...  # see [Ellipsis](#ellipsis)

Did you notice the three dots (...) above? In case you don't already know them, check out the section below to learn more about them.

Using OpenAPI Generator with the typescript-fetch generator lead to the following code:

/**
 * Gütestufeskala gemäss Mini-ICF-APP Work.
 * @export
 */
export const GuetestufeWahl = {
    /**
    * 0: Keine Beeinträchtigung
    */
    KEINE_BEEINTRAECHTIGUNG: 0,
    /**
    * 1: Leichte Beeinträchtigung (ohne Negativfolgen)
    */
    LEICHTE_BEEINTRAECHTIGUNG: 1,
    /**
    * 2: Mässige Beeinträchtigung (Negativfolgen selbst behebbar)
    */
    MAESSIGE_BEEINTRAECHTIGUNG: 2,
    /**
    * 3: Erhebliche Beeinträchtigung (Assistenz für Folgenbehebung nötig)
    */
    ERHEBLICHE_BEEINTRAECHTIGUNG: 3,
    /**
    * 4: Vollständige Beeinträchtigung (Assistenz immer nötig)
    */
    VOLLSTAENDIGE_BEEINTRAECHTIGUNG: 4
} as const;
export type GuetestufeWahl = typeof GuetestufeWahl[keyof typeof GuetestufeWahl];

Ellipsis

The three dots ... are called Ellipsis. Because it's a builtin constant, the code above is valid Python code, I shit you not. (And this was my favorite, yet a bit unprofessional, English phrase.)

You could also replace the Ellipsis with pass and it would do exactly the same thing (which is actually, nothing except leading to syntactically correct code. Check out my question on Stackoverflow I asked once, when I was new to Python.