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:
-
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.
-
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
- 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
- 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
Description
When djantic2 extracts fields from a Django model that uses
TextChoices(which inherits fromStrEnum), it creates plainEnumtypes instead ofStrEnum, and sets defaults as plain strings instead of enum instances. This causesPydanticSerializationUnexpectedValuewarnings during serialization.Root Cause
In
djantic/fields.pylines 147-154:Two issues:
Enum()instead ofStrEnum()— The generated enum doesn't inherit fromstr, so Pydantic treats string values and enum instances as different types. Django'sTextChoicesinherits fromStrEnum, but djantic2's generated enum does not preserve this.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
Expected Behavior
str(i.e., useStrEnumor passstras a mixin) when the Django field's choices come fromTextChoicesSuggested Fix
For Python <3.11 compatibility, the enum could be created with
Enum(name, choices, type=str).Environment
Workaround
We work around this in our consuming code by coercing string defaults to enum instances after extracting fields from the ModelSchema: