Skip to content

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() and model_dump_json() for native Pydantic serialization
  • model_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 annotations import 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

  1. Update dependencies:

    poetry add protean@"^0.15.0"
    
  2. Search for from protean.fields import List usage. If List is used for Value Object embedding, change to ValueObjectList.

  3. Search for VO constructors that pass extra kwargs. Remove extra arguments.

  4. If using from __future__ import annotations, switch field definitions to assignment style in affected modules.

  5. Run your test suite. Most code will pass without changes.

  6. 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(), selective include/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.