--- title: "MongoDB Enum Deserialization to String" tags: [mongodb, pydantic, python, enum, fastapi, bug-pattern] sources: [99 Daily/2026-04-29.md] created: 2026-04-30 updated: 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 ```python 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: ```python 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) ```python 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: ```python 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)` ## Related - [[wiki/concepts/pydantic-default-factory-type-alias|pydantic-default-factory-type-alias]] — another silent Pydantic/MongoDB mismatch - [[wiki/tech-patterns/fastapi-patterns|fastapi-patterns]] — FastAPI error handling