Three problems nearly broke Vellum during development: a metaclass conflict, a build system trap, and a subtle data mutation bug. Here’s how each one happened and how we fixed it.
1. The Metaclass Conflict
We needed three things simultaneously:
- Pydantic’s
BaseModelfor field validation - A
HooksMixinfor lifecycle hooks - A custom metaclass to intercept field references
The problem: you can’t reference Product.name inside the class body — Product doesn’t exist yet. The class definition order prevents forward references to incomplete classes.
The Solution: Two APIs
Rather than forcing incompatible inheritance, we implemented two complementary patterns:
Settings.indexes — static definitions using string references, safe inside the class body:
class Product(VellumBaseModel):
name: str
price: float
class Settings:
indexes = [
Index("name"),
Index(("price", 1), ("name", 1)),
]
__init_indexes__ — a classmethod that runs after class creation, where type-safe field references are valid:
class Product(VellumBaseModel):
name: str
price: float
@classmethod
def __init_indexes__(cls):
return [
Index(cls.name),
Index(cls.price, cls.name),
Index(cls.price.desc()),
]
The VellumMetaclass invokes __init_indexes__ post-initialization and merges results with static definitions.
2. The Build System Trap
Using poetry-core with a src/ directory layout and hyphenated package naming (vellum-odm) prevented proper package discovery during builds entirely.
The Fix: Switch to Hatchling
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "vellum-odm"
version = "0.1.0"
Hatchling natively handles PEP 621, src/ layouts, and the distinction between package names (hyphenated) and import names (underscores) — automatically.
3. The $push Bug
MongoDB’s $push operator behaves differently for single values vs. arrays. Our UpdateBuilder.push() incorrectly wrapped single values in arrays:
# Bug: tags became ["mobile", "5g", ["sale"]] instead of ["mobile", "5g", "sale"]
The Fix
Differentiate single and multiple values explicitly:
if self._pushes:
push_update = {}
for f, values in self._pushes.items():
if len(values) == 1:
push_update[f] = values[0] # single: {"$push": {"tags": "sale"}}
else:
push_update[f] = {"$each": values} # multiple: {"$push": {"tags": {"$each": [...]}}}
update["$push"] = push_update