Migration Files
Migration files are Python modules that record how your API changed between versions. They serve two purposes:
- Schema deltas — What operations and schemas were added, removed, or modified
- Data migrations — How to transform data between the old and new formats
File Structure
Migration files follow this naming pattern:
For example: m_0001_initial_api.py, m_0002_add_phone_field.py
Anatomy of a Migration File
Here's a complete migration file:
"""
API migration: 1 -> 2
Add phone field to PersonOut schema
"""
from crane.delta import VersionDelta
from crane.data_migrations import (
DataMigrationSet,
SchemaDowngrade,
SchemaUpgrade,
)
# Dependencies on previous migrations
dependencies: list[tuple[str, str]] = [("myapp.api_migrations.default", "1")]
# Version transition
from_version: str | None = "1"
to_version: str = "2"
# Schema delta (auto-generated)
delta = VersionDelta.model_validate_json("""
{
"actions": [
{
"action": "schema_definition_modified",
"schema_ref": "#/components/schemas/PersonOut",
"old_schema": {
"properties": {
"name": {"type": "string"}
},
"required": ["name"]
},
"new_schema": {
"properties": {
"name": {"type": "string"},
"phone": {"type": "string"}
},
"required": ["name"]
}
}
]
}
""")
# Data transformers (you implement these)
def downgrade_person_out(data: dict) -> dict:
"""2 -> 1: Remove phone for v1 clients."""
data.pop("phone", None)
return data
def upgrade_person_out(data: dict) -> dict:
"""1 -> 2: Add default phone for v1 requests."""
data.setdefault("phone", None)
return data
data_migrations = DataMigrationSet(
schema_downgrades=[
SchemaDowngrade("#/components/schemas/PersonOut", downgrade_person_out),
],
schema_upgrades=[
SchemaUpgrade("#/components/schemas/PersonOut", upgrade_person_out),
],
)
Required Attributes
dependencies
A list of (module_path, version) tuples that this migration depends on:
The first migration has an empty list. Subsequent migrations depend on the previous version.
Currently unused, but tracked to later allow merge API migrations, similar to python manage.py makemigrations --merge.
from_version and to_version
The version transition this migration represents:
delta
A VersionDelta object containing all schema changes. This is auto-generated by makeapimigrations and shouldn't be
edited manually:
Delta Actions
The delta.actions list contains these action types:
Operation Changes
| Action | Description |
|---|---|
operation_added |
New endpoint added |
operation_removed |
Endpoint deleted |
operation_modified |
Endpoint parameters or responses changed |
Schema Changes
| Action | Description |
|---|---|
schema_definition_added |
New schema type added |
schema_definition_removed |
Schema type deleted |
schema_definition_modified |
Schema fields or types changed |
Data Migrations
The data_migrations object contains transformers for runtime data conversion.
Note
You'll usually not have to write any of the transformers described below. In most cases they are
automatically generated by the makeapimigrations command and created in your migrations file.
The documentation below is if you ever have a case you need to manually describe in your migration files, and so you can read and understand the contents of a migration file, to check if the auto-generated logic is as you'd expect.
Schema Transformers
Schema transformers apply wherever that schema appears—in responses, nested objects, or array items:
data_migrations = DataMigrationSet(
schema_downgrades=[
# Applied to responses when downgrading (new → old)
SchemaDowngrade("#/components/schemas/PersonOut", downgrade_person),
],
schema_upgrades=[
# Applied to requests when upgrading (old → new)
SchemaUpgrade("#/components/schemas/PersonIn", upgrade_person),
],
)
Path Rewrites
When an endpoint URL changes, the migration generator will add a path rewrite:
from crane.data_migrations import PathRewrite
data_migrations = DataMigrationSet(
path_rewrites=[
PathRewrite(
old_path="/persons/{person_id}",
new_path="/people/{person_id}",
methods=["get", "put", "delete"],
),
],
)
Operation Transformers
For endpoint-specific transformations that can't be expressed at the schema level:
from crane.data_migrations import OperationUpgrade, OperationDowngrade
data_migrations = DataMigrationSet(
operation_upgrades=[
OperationUpgrade("/users", "post", upgrade_create_user),
],
operation_downgrades=[
OperationDowngrade("/users", "get", downgrade_list_users),
],
)
See Operation Transformers for details.
Transformer Functions
Downgrade Transformers
Convert response data from the new schema to the old schema:
def downgrade_person_out(data: dict) -> dict:
"""Remove fields that didn't exist in the old version."""
data.pop("phone", None) # phone was added in this version
return data
Tip
The downgrade function receives data in the new format and must return data in the old format. That means you need to consider how to transform this schema if it is used in a response body.
Upgrade Transformers
Convert request data from the old schema to the new schema:
def upgrade_person_in(data: dict) -> dict:
"""Add fields with defaults for old clients."""
data.setdefault("phone", None) # provide default for new field
return data
Tip
The upgrade function receives data in the old format and must return data in the new format. That means you need to consider how to transform this schema if it is used in a request body
Async Transformers
Transformers can be sync or async, according to your preference.
async def upgrade_person_in(data: dict) -> dict:
if "legacy_id" in data:
# Look up the new ID
new_id = await lookup_new_id(data.pop("legacy_id"))
data["id"] = new_id
return data
Async is slightly better performance
The middleware itself is written to support async operation, and wraps non-coroutine transformers
in a sync_to_async call to make sure the event loop is not blocked by blocking I/O, if any.
That does mean there's a small (creating a thread) overhead to sync transformers.
If you want the best performance, write your transformers async. If you're not familiar (enough) with async python, it's still fine to write them sync. I won't judge, hell, it's the default option.
Schema References
Schema references use the OpenAPI JSON pointer format:
This matches the schema name in your Django Ninja schemas:
Generation vs Manual Editing
The delta is auto-generated and shouldn't be edited. The data_migrations require manual implementation—the generator
creates skeleton functions with NotImplementedError for breaking changes.
Warning
Don't edit the delta JSON manually. If you need to change what's tracked, modify your API and regenerate.
Migration Chain
Migrations form a chain through their dependencies. When reconstructing API state at a version, crane applies migrations in sequence:
To get the API schema at v2, crane applies m_0001 and m_0002 but not m_0003.
Next Steps
- Modifying a Schema — Step-by-step example
- Path Rewrites — Handle URL changes
- Operation Transformers — Advanced transformation patterns