JOLT DATA SDK GUIDE · 03
Schema migrations
Jolt data can outlive one version of an application. A migration turns an older stored value into the one current Schema Class before application code sees it.
You keep Post, not PostV1, PostV2, and PostV3. Historical shapes stay
inside small, deterministic migration steps.
Declare each step
Suppose version 1 called its text field message. Version 2 renamed that field
to text. Version 3 added a required list of tags.
import {
Collection,
Field,
Migrations,
Read,
Schema,
SchemaMigrationError,
} from "jolt-sdk/data";
export const PostMigrations = Migrations.create()
.to(2, value => Migrations.rename(value, { message: "text" }))
.to(3, value => ({
...value,
tags: value.tags ?? [],
}));
@Schema({ version: 3, migrations: PostMigrations })
export class Post {
@Field.string()
text!: string;
@Field.array(Field.string)
tags!: string[];
@Field.dateTime()
postedAt!: Date;
}
export const Posts = Collection.create(Post, {
access: {
read: Read.AnyIdentity,
create: true,
update: true,
},
});
export function explainMigrationFailure(error: unknown): string {
if (error instanceof SchemaMigrationError) {
return `Could not upgrade schema ${error.fromVersion} to ${error.toVersion}`;
}
throw error;
}Migrations.rename() returns a new value. It never adds helper fields to your
application data. A normal .to(...) step can perform any other pure value
transformation.
What happens on a read
When the Data SDK reads an older record, it:
- reads the stored schema version;
- applies every required step in order;
- validates the result against the current Schema Class; and
- gives application code only the current
Posttype.
The daemon and protocol do not interpret the schema. Migration belongs to the application-facing SDK boundary.
Test old data directly
Use the Resource's migrate() helper in a focused test. Pass the historical
version and a plain stored value; do not recreate old model classes.
import { describe, expect, it } from "vitest";
import { Post, Posts } from "./migrations";
describe("Data SDK migrations guide", () => {
it("migrates version one data into the current Schema Class", () => {
const post = Posts.migrate({
version: 1,
value: {
message: "Hello from version one",
postedAt: "2026-08-31T12:00:00.000Z",
},
});
expect(post).toBeInstanceOf(Post);
expect(post.text).toBe("Hello from version one");
expect(post.tags).toEqual([]);
expect(post.postedAt).toEqual(new Date("2026-08-31T12:00:00.000Z"));
});
});This helper exists for migration tests and lower-level tooling. Ordinary reads apply migrations automatically.
Keep migrations boring
A migration must produce the same output every time. It must not read the clock, call the network, inspect the current user, or mutate its input.
Every version between the stored value and the current schema needs a step. A
missing step, a thrown transformation, or a final value that does not validate
throws SchemaMigrationError. Catch that error by type when a tool or advanced
application needs to report the failed version range.
Migrations.rename() also refuses to overwrite an existing destination or
send two old fields to the same new field. Failing is safer than silently
discarding stored data.
Continue learning
- Return to Data SDK fundamentals.
- Learn the typed lifecycle in Item mutations.
- See migrations in the generated API reference.
- Build the complete beginner Chirp app.