Migrating to 0.15
From 0.14 to 0.15
Protean 0.15 rebuilds the domain element internals on Pydantic v2. Your existing code continues to work in most cases — assignment-style field definitions are fully preserved. This guide covers what changed, what might break, and how to update.
What changed
Domain elements are now Pydantic models
Every Aggregate, Entity, Value Object, Command, Event, and Projection now
inherits from Pydantic's BaseModel. This is an internal change — the
public API (to_dict(), @domain.aggregate, @invariant, etc.) is
unchanged.
What this gives you:
- Rust-powered validation via pydantic-core
model_dump()andmodel_dump_json()for native Pydantic serializationmodel_json_schema()for JSON Schema generation- Compatibility with tools that consume Pydantic models (FastAPI, schema registries, etc.)
Three field definition styles
Protean now supports three ways to define fields. All produce identical runtime behavior.
@domain.aggregate
class Product:
# 1. Annotation style (new, recommended)
name: String(max_length=50, required=True)
# 2. Assignment style (existing Protean/Django convention)
price = Float(min_value=0)
# 3. Raw Pydantic style (escape hatch)
metadata: Annotated[dict, Field(default_factory=dict)]
Your existing assignment-style code continues to work. No migration is required for field definitions unless you want to adopt the new annotation style.
FieldSpec abstraction
Field functions (String, Integer, Float, etc.) now return FieldSpec
objects instead of Field descriptor instances. FieldSpecs are resolved
into Pydantic's Annotated[type, Field(...)] at class creation time and
disappear before Pydantic processes the class.
This is an internal change. If your code only uses field functions in class bodies (as intended), no changes are needed.
Breaking changes
1. Pydantic v2 is now required
Protean 0.15 requires pydantic>=2.10,<3.0. Install or upgrade:
poetry add pydantic@">=2.10,<3.0"
# or
pip install "pydantic>=2.10,<3.0"
2. List import changed
Previously, from protean.fields import List gave you the Value Object list
descriptor. Now it returns a FieldSpec factory for generic typed lists.
If you use List for embedding Value Object lists, update your import:
# Before (0.14 and earlier)
from protean.fields import List
items = List(content_type=OrderItem)
# After (0.15)
from protean.fields import ValueObjectList
items = ValueObjectList(content_type=OrderItem)
The new List() factory works the same way for generic lists:
from protean.fields import List
tags = List(content_type=String) # Still works
3. Value Objects reject unknown fields
BaseValueObject now uses extra="forbid" in its Pydantic model config.
Code that passed extra keyword arguments to VO constructors will now raise
ValidationError:
# Before: silently ignored extra fields
address = Address(street="123 Main", city="NYC", extra_field="ignored")
# After: raises ValidationError
address = Address(street="123 Main", city="NYC", extra_field="rejected")
Fix: Remove extra keyword arguments from VO construction calls.
4. from __future__ import annotations incompatibility
Annotation-style field definitions do not work with PEP 563 deferred evaluation:
from __future__ import annotations # Breaks annotation-style fields
@domain.aggregate
class Product:
name: String(max_length=50) # Becomes the string "String(max_length=50)"
Workarounds:
- Use assignment style:
name = String(max_length=50) - Use raw Pydantic style:
name: Annotated[str, Field(max_length=50)] - Remove the
from __future__ import annotationsimport if not needed
This limitation will be resolved when Python 3.14 ships PEP 649 (lazy evaluation of annotations).
What to check
Serialization: to_dict() vs model_dump()
Both methods are now available. They may produce different output in some cases:
| Aspect | to_dict() |
model_dump() |
|---|---|---|
| Reference fields | Skipped | May be included |
| Shadow fields | Included via ResolvedField | Not included (not Pydantic fields) |
| DateTime values | Converted to strings | Returns datetime objects (by default) |
| Nested VOs | Recursively serialized | Recursively serialized |
Recommendation: Continue using to_dict() for Protean's standard
serialization. Use model_dump() when interacting with Pydantic-native
tools.
Validation timing
Pydantic validates fields during __init__() and on every attribute
assignment (via validate_assignment=True). If your code relied on
setting invalid values temporarily and fixing them before persistence,
this will now raise ValidationError immediately.
IDE setup
Annotation-style fields place FieldSpec instances in annotation positions,
which confuses some type checkers:
Pyright/Pylance: Add to your project root:
// pyrightconfig.json
{
"reportInvalidTypeForm": false
}
mypy: Enable the Protean plugin:
# pyproject.toml
[tool.mypy]
plugins = ["protean.ext.mypy_plugin"]
Step-by-step upgrade
-
Update dependencies:
poetry add protean@"^0.15.0" -
Search for
from protean.fields import Listusage. IfListis used for Value Object embedding, change toValueObjectList. -
Search for VO constructors that pass extra kwargs. Remove extra arguments.
-
If using
from __future__ import annotations, switch field definitions to assignment style in affected modules. -
Run your test suite. Most code will pass without changes.
-
Optional: Adopt annotation-style fields in new or updated code for a cleaner, more declarative syntax.
New features to explore
- JSON Schema: Call
MyAggregate.model_json_schema()for automatic schema generation. - Native Pydantic serialization:
model_dump(exclude_none=True),model_dump_json(), selectiveinclude/exclude. - Raw Pydantic escape hatch: Use
Annotated[type, Field(...)]for any Pydantic feature not covered by Protean's field functions. - mypy plugin: Full type inference for field functions in IDEs that support mypy.