---
title: "Pydantic default_factory with Type Alias Yields Empty String"
aliases: [pydantic-default-factory, pyobjectid-empty, pydantic-objectid-bug]
tags: [pydantic, mongodb, python, bug, objectid]
sources:
  - "daily/2026-04-29.md"
created: 2026-04-29
updated: 2026-04-29
---

# Pydantic `default_factory` with Type Alias Yields Empty String

When `PyObjectId` is defined as a type alias `Annotated[str, ...]` and used directly as `default_factory=PyObjectId`, Python calls `str()` (the base constructor, not `ObjectId()`), which returns an empty string `""`. Because MongoDB's `_id` field has a unique index, every document after the first that omits an explicit `_id` will trigger a `DuplicateKeyError` on `""`. The fix is always `lambda: str(ObjectId())`.

## Key Points

- `PyObjectId = Annotated[str, BeforeValidator(...)]` is a type alias, NOT a callable that generates new IDs
- Calling `PyObjectId()` resolves to `str()` → returns `""`
- MongoDB inserts the empty string as `_id`, creating a duplicate key conflict on the second insert
- The error message is `E11000 duplicate key error collection: ... index: _id_ dup key: { _id: "" }` — the `""` is the diagnostic signal
- Always use `default_factory=lambda: str(ObjectId())` for MongoDB ID fields

## Details

```python
from bson import ObjectId
from pydantic import BaseModel, Field
from typing import Annotated
from pydantic.functional_validators import BeforeValidator

# Type alias — NOT a constructor for new IDs
PyObjectId = Annotated[str, BeforeValidator(str)]

# ❌ BAD — default_factory=PyObjectId calls str() → ""
class BadDocument(BaseModel):
    id: PyObjectId = Field(default_factory=PyObjectId, alias="_id")
    name: str

# ✅ GOOD — lambda generates a real unique ObjectId on each call
class GoodDocument(BaseModel):
    id: PyObjectId = Field(default_factory=lambda: str(ObjectId()), alias="_id")
    name: str
```

### Why This Happens

Python evaluates `default_factory=PyObjectId` as "call this object with no arguments at instantiation time." Since `PyObjectId` is `Annotated[str, ...]`, its runtime type is `str`. Calling `str()` with no arguments returns `""`.

```python
# Proof:
PyObjectId = Annotated[str, lambda x: x]
print(type(PyObjectId))   # <class 'str'>
print(PyObjectId())       # "" ← empty string, not an ObjectId
```

### Full Correct Pattern

```python
from bson import ObjectId
from pydantic import BaseModel, Field
from pydantic.functional_validators import BeforeValidator
from typing import Annotated, Optional

PyObjectId = Annotated[str, BeforeValidator(str)]

class Document(BaseModel):
    id: Optional[PyObjectId] = Field(
        default_factory=lambda: str(ObjectId()),
        alias="_id"
    )

    model_config = {"populate_by_name": True}
```

### Diagnosis Checklist

If you see `DuplicateKeyError` with `dup key: { _id: "" }`:
1. Find all models with `id: PyObjectId = Field(default_factory=PyObjectId, ...)`
2. Replace with `default_factory=lambda: str(ObjectId())`
3. Check if the collection already has a document with `_id: ""` — delete it manually

## Related Concepts

- [[wiki/concepts/pydantic-v2-alias-id-gotcha]] — related Pydantic v2 alias serialization gotcha with `_id`
- [[wiki/tech-patterns/fastapi-python-docker]] — FastAPI + MongoDB stack where this pattern appears

## Sources

- [[daily/2026-04-29.md]] — Traced from `DuplicateKeyError` on second insert in a FastAPI/MongoDB service
