obsidian/wiki/concepts/mongodb-enum-deserialization.md
2026-04-30 21:18:40 +01:00

2.6 KiB

title tags sources created updated
MongoDB Enum Deserialization to String
mongodb
pydantic
python
enum
fastapi
bug-pattern
99 Daily/2026-04-29.md
2026-04-30 2026-04-30

MongoDB Enum Deserialization to String

MongoDB stores Python Enum fields as their raw string (or int) values. When a document is read back and deserialized into a Pydantic model, the field may arrive as a plain str instead of the Enum member — depending on the Pydantic version, ODM, and field configuration.

The Bug

class UserRole(str, Enum):
    ADMIN = "admin"
    VIEWER = "viewer"

class User(BaseModel):
    role: UserRole

After reading from MongoDB, user.role may be "admin" (a str), not UserRole.ADMIN. Calling .value on it then crashes:

user.role.value  # AttributeError: 'str' object has no attribute 'value'

This 500 is often silent in production — FastAPI catches it generically and logs it as an unhandled exception, not obviously linked to the Enum field.

Root Cause

The crash occurred in audit_logger.py:82 calling user.role.value under the assumption that role was a Python Enum. MongoDB had stored it as "admin" and the ODM passed the raw string through without coercion.

Guard Pattern (Short-term)

role_value = user.role.value if hasattr(user.role, "value") else user.role

Works at any call site but is defensive noise. Prefer the boundary fix below.

Boundary Fix (Long-term)

Coerce at deserialization time using a Pydantic validator so all downstream code can safely assume the field is the right type:

from pydantic import field_validator

class User(BaseModel):
    role: UserRole

    @field_validator("role", mode="before")
    @classmethod
    def coerce_role(cls, v):
        if isinstance(v, UserRole):
            return v
        return UserRole(v)  # raises ValueError if value is unknown

With Pydantic v2 and model_config = ConfigDict(use_enum_values=True) the Enum is stored as its value — reading it back requires the validator to re-wrap it.

When It Happens

  • Motor / PyMongo (no ODM) — always returns raw BSON types; no Enum coercion
  • Beanie ODM — coerces correctly in most cases; can break with use_enum_values=True
  • Pydantic v1 orm_mode — coerces from raw value automatically
  • Pydantic v2 — requires explicit mode="before" validator or ConfigDict(use_enum_values=False)