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

74 lines
2.6 KiB
Markdown

---
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