Skip to content

Quickstart - Schema Migration

Schema evolves over time.

SpecStar helps you handle schema changes safely — without losing data.

⏱️ Estimated time: 5 minutes


When do you need migration?

Schema changes can be divided into two categories:

  • Backward compatible changes (no migration needed)
  • Breaking changes (migration required)

1. Backward compatible changes

Some schema changes are automatically handled by SpecStar at runtime.

These include:

  • adding new attributes with default values
  • changing default values
  • expanding Union / Literal types with new variants

Example

Original schema:

class Issue(msgspec.Struct):
    title: str
    description: str | None = None
    status: Literal["open", "in_progress", "resolved"] = "open"

Updated schema:

class Issue(msgspec.Struct):
    title: str
    description: str | None = None
    status: Literal["open", "in_progress", "resolved"] = "open"
    priority: Literal["low", "medium", "high"] = "medium"

No migration is required.

When SpecStar reads older data, missing fields are automatically filled using default values.


2. Breaking changes

Breaking changes require explicit migration.

These include:

  • adding new attributes without default values
  • removing existing attributes
  • narrowing Union / Literal types (removing variants)
  • changing field types

3. Define versioned schemas

Keep the old schema and introduce a new version.

class IssueV1(msgspec.Struct):
    title: str
    description: str | None = None
    status: Literal["open", "in_progress", "resolved"] = "open"


class Issue(msgspec.Struct):  # v2
    title: str
    priority: Literal["low", "medium", "high"]  # new required field
    description: str | None = None
    status: Literal["open", "in_progress", "resolved"] = "open"

4. Define migration logic

Write a function that converts old data into the new schema.

def migrate_v1_to_v2(old: IssueV1) -> Issue:
    return Issue(
        title=old.title,
        priority="medium",  # default for existing data
        description=old.description,
        status=old.status,
    )

5. Register the migration

Attach the migration step when registering the new schema:

from specstar import spec, Schema

spec.configure()
spec.add_model(
    Schema(Issue, "v2").step(
        "v1",
        migrate_v1_to_v2,
        source_type=IssueV1,
    )
)

This tells SpecStar how to upgrade data from v1v2.


6. Execute migration

Run migration via API:

curl -X POST http://127.0.0.1:8000/issues/migrate/execute \
  -H "Content-Type: application/json" \
  -d '{
    "limit": 10000
  }'

This will:

  • scan existing resources
  • convert old revisions using your migration function
  • store them as new revisions in the latest schema

7. What happens after migration?

After migration:

  • all new writes use the latest schema (v2)
  • old data is preserved as historical revisions
  • your system continues to support revision history seamlessly

Why this matters

Schema changes are one of the hardest parts of maintaining a system.

With SpecStar:

  • backward-compatible changes require no action
  • breaking changes are explicit and controlled
  • migration logic is versioned alongside your schema
  • historical data is never lost

You evolve your schema without breaking your system.


What’s next

If your application is evolving in place, backend setup becomes an important follow-up so migrations run against durable storage.

Next Steps: