Skip to content

External Schema Families

An external SchemaFamily keeps schema history beside the application model instead of stacking an ever-growing declaration above it. This is the primary configuration style for histories that need their own module, tooling, or more than one interpretation of the same current model.

Keep the current model ordinary

The model module does not need to import pydantic_versions:

# models.py
from pydantic import BaseModel


class AppConfig(BaseModel):
    timeout: float = 10.0
    retries: int = 3
    new_feature: bool = False

Declare its history elsewhere:

# schema_history.py
from models import AppConfig
from pydantic_versions import (
    SchemaFamily,
    SchemaVersion,
    VersionTransition,
    field_default,
    field_removed,
)


def upgrade_v1(data: dict) -> dict:
    data.setdefault("new_feature", False)
    return data


APP_CONFIG_SCHEMA = SchemaFamily(
    model=AppConfig,
    name="app_config",
    versions=(
        SchemaVersion(
            "1",
            patches=(
                field_default("timeout", 5.0),
                field_removed("new_feature"),
            ),
        ),
        SchemaVersion("2"),
    ),
    transitions=(
        VersionTransition("1", "2", upgrade=upgrade_v1),
    ),
)

The final declared label is current. Historical versions are projected independently from the current model; patches are not accumulated from one historical declaration into the next.

Use the family explicitly

Direct family calls cannot be confused with another history:

result = APP_CONFIG_SCHEMA.validate(
    {"schema_version": "1", "retries": 2},
)

assert result.current_model == AppConfig(
    timeout=5.0,
    retries=2,
    new_feature=False,
)

v1_model = APP_CONFIG_SCHEMA.model_for("1")
v1_defaults = APP_CONFIG_SCHEMA.defaults_for(version="1")

The compatibility free functions also accept a family as their first argument:

from pydantic_versions import model_for_version, validate_versioned


v1_model = model_for_version(APP_CONFIG_SCHEMA, "1")
result = validate_versioned(
    APP_CONFIG_SCHEMA,
    {"schema_version": "1", "retries": 2},
)

Reuse one model in two families

Families own their declarations, compiled projections, transitions, and generated-model cache. Two families can therefore reuse the same application model without overwriting each other:

PUBLIC_CONFIG = SchemaFamily(
    model=AppConfig,
    name="public_config",
    versions=(SchemaVersion("1"), SchemaVersion("2")),
)
INTERNAL_CONFIG = SchemaFamily(
    model=AppConfig,
    name="internal_config",
    versions=(SchemaVersion("legacy"), SchemaVersion("current")),
)

Constructing either family does not select it globally. A model-only call has no basis for choosing and raises SchemaFamilySelectionError.

Select one compatibility default deliberately

Applications that still need model-only helper calls can select one family during configuration startup:

PUBLIC_CONFIG.as_default()

After that call, validate_versioned(AppConfig, data) delegates to PUBLIC_CONFIG. Repeating as_default() on the same family is safe. Selecting a different second default raises SchemaFamilySelectionError and leaves the first selection unchanged.

The @versioned_schema compatibility decorator performs this selection for its own family automatically. External family construction never does.

Compilation rules

Compilation happens on first use or through an explicit compile() call. It is thread-safe and idempotent; repeated calls on one family return the same family and generated model objects.

Declarations are copied into immutable tuples before compilation. Labels must already be non-empty strings, the final label is current, and custom transitions must connect adjacent forward labels. Duplicate, reverse, skipped, or otherwise unreachable transitions fail instead of becoming silent dead code.

The current foundation covers flat patch projections and forward upgrades. Explicit historical wire models, explicit nested-family mappings, and downgrade execution are rejected rather than silently ignored until their dedicated 0.2 runtime support lands.