Skip to content

Enum fields use plain Enum instead of StrEnum and set defaults as strings #32

@mecampbellsoup

Description

@mecampbellsoup

Description

When djantic2 extracts fields from a Django model that uses TextChoices (which inherits from StrEnum), it creates plain Enum types instead of StrEnum, and sets defaults as plain strings instead of enum instances. This causes PydanticSerializationUnexpectedValue warnings during serialization.

Root Cause

In djantic/fields.py lines 147-154:

python_type = Enum(  # type: ignore
    f"{enum_prefix}Enum",
    enum_choices,
    module=__name__,
)

if field.has_default() and isinstance(field.default, Enum):
    default = field.default.value

Two issues:

  1. Enum() instead of StrEnum() — The generated enum doesn't inherit from str, so Pydantic treats string values and enum instances as different types. Django's TextChoices inherits from StrEnum, but djantic2's generated enum does not preserve this.

  2. Default is set to .value (a string) — Line 154 converts the enum default to its string value. Since Pydantic doesn't validate defaults by default, the string bypasses coercion and remains a plain string in the model instance.

Reproduction

from django.db import models
from djantic import ModelSchema
from pydantic import ConfigDict

class MyModel(models.Model):
    class Status(models.TextChoices):
        ACTIVE = "active", "Active"
        INACTIVE = "inactive", "Inactive"

    status = models.CharField(max_length=20, choices=Status.choices, default=Status.ACTIVE)

    class Meta:
        app_label = "example"

class MySchema(ModelSchema):
    model_config = ConfigDict(model=MyModel)

# Check the generated field:
info = MySchema.model_fields["status"]
print(info.annotation)          # <enum 'MySchemaStatusEnum'> — plain Enum, not StrEnum
print(info.annotation.__mro__)  # No 'str' in the MRO
print(type(info.default))       # <class 'str'> — not an enum instance
print(repr(info.default))       # 'active' — plain string

# This triggers PydanticSerializationUnexpectedValue warnings:
import warnings
with warnings.catch_warnings(record=True) as w:
    warnings.simplefilter("always")
    MySchema().model_dump(mode="json")
    print(w)  # Warning about expected enum but got str

Expected Behavior

  1. The generated enum should inherit from str (i.e., use StrEnum or pass str as a mixin) when the Django field's choices come from TextChoices
  2. The default should be an instance of the generated enum, not a plain string

Suggested Fix

# 1. Use StrEnum instead of plain Enum (Python 3.11+)
from enum import StrEnum

python_type = StrEnum(  # type: ignore
    f"{enum_prefix}Enum",
    enum_choices,
    module=__name__,
)

# 2. Coerce default to enum instance
if field.has_default() and isinstance(field.default, Enum):
    default = python_type(field.default.value)

For Python <3.11 compatibility, the enum could be created with Enum(name, choices, type=str).

Environment

  • Python: 3.14
  • djantic2: 1.0.5
  • Pydantic: 2.11+
  • Django: 5.2

Workaround

We work around this in our consuming code by coercing string defaults to enum instances after extracting fields from the ModelSchema:

import enum

def _extract_fields(schema):
    fields = {}
    for name, info in schema.model_fields.items():
        if (
            isinstance(info.default, str)
            and isinstance(info.annotation, type)
            and issubclass(info.annotation, enum.Enum)
        ):
            info.default = info.annotation(info.default)
        fields[name] = (info.annotation, info)
    return fields

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions