How It Works
django-ninja-crane enables API versioning through three mechanisms:
- Migration files store schema deltas between versions
- State reconstruction rebuilds the API schema at any historical version
- Runtime transformation converts data between versions on every request
At Development time, you use python manage.py makeapimigrations to detect differences between the stored
schema deltas and your code. Then, you fill in the generated migration file's
transformers (if any), to tell the versioning middleware how to "upgrade" from this version to the latest version.
Then, at Runtime, the migration files you made are used when a request for an older version is encountered:
Step by Step
- Version extraction — The middleware reads the
X-API-Versionheader - Upgrade loop — For each migration between extracted version and latest, apply upgrade transformers (path rewrites, request body transforms)
- Endpoint execution — Your code runs with the latest format, unaware of versioning
- Downgrade loop — For each migration from latest back to extracted version, apply downgrade transformers (response body transforms)
State Reconstruction
Migrations store deltas, not full schemas. To get the API schema at version N, we apply all existing deltas:
This allows our makeapimigrations command to compare the state resulting from all deltas against the state in your
code.
That difference can also be expressed as a version delta, and is what's saved into the newest migration file.
The state stored in the migration deltas does not cover all possible OpenAPI fields. To allow you to add examples, auth, ..., the runtime uses deltas backwards: It takes the OpenAPI JSON currently generated by your NinjaAPI, and applies the inverse of each delta to get to the target version.
Transformation Engine
Schema Transformers
Schema transformers apply recursively wherever a schema appears:
# PersonOut might appear in:
# - Direct response body
# - Nested in TeamOut.leader
# - Items in list[PersonOut]
# - Union types: PersonOut | CompanyOut
The engine traverses the response structure and applies the appropriate transformer at each location.
Transformation Order
For a request from v1 to a v3 API:
v1 request → upgrade(m_0002) → upgrade(m_0003) → v3 endpoint
v3 response → downgrade(m_0003) → downgrade(m_0002) → v1 response
Migrations are applied in sequence, forwards for upgrades, backwards for downgrades.
This allows each transformer to be written in isolation: it only needs to concern itself how to get a schema from version n to version n+1, and back.
Middleware Details
The VersionedAPIMiddleware:
- Discovers APIs — Finds all
VersionedNinjaAPIinstances by introspecting URL patterns - Caches migrations — Loads migrations once, caches API states
- Handles async — Supports both sync and async Django
- Preserves headers — Copies response headers through transformations
Per-Request State
The middleware attaches version info to the request:
request.api_version # "1" - the resolved version
request.api_latest_version # "3" - the current latest
request.original_path # "/persons/1" - before rewriting (if rewritten)
OpenAPI Generation
The versioned OpenAPI generator:
- Loads migrations up to the requested version
- Reconstructs the API state at that version
- Generates an OpenAPI schema reflecting that state
This means /api/docs?version=1 shows the API as it was at v1, with old paths, old schemas, and old parameters.
Why Deltas Instead of Full Schemas?
Storing full schemas at each version would be simpler, but results in much larger migration files:
- Most changes are small (add a field, rename an endpoint)
- Storing the JSON of each state would also include all the JSON schema data for unchanged endpoints and schemas.
- Deltas make diffs obvious and reviewable
The tradeoff is that reconstructing state requires applying all migrations in sequence. This is cached in production, so the cost is paid once per process startup.