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