| title | Examples |
|---|---|
| description | Common migration patterns: renaming, defaults, restructuring, async |
| sidebar_order | 4 |
Common migration patterns for transforming JSON data.
Split a single name field into firstName and lastName:
import { createMigrations, migrate } from "@nanocollective/json-up";
import { z } from "zod";
const migrations = createMigrations()
.add({
version: 1,
schema: z.object({ name: z.string() }),
up: (data) => ({ name: data.name ?? "Unknown" }),
})
.add({
version: 2,
schema: z.object({ firstName: z.string(), lastName: z.string() }),
up: (data) => {
const parts = data.name.split(" ");
return {
firstName: parts[0] ?? "",
lastName: parts.slice(1).join(" "),
};
},
})
.build();
const result = migrate({
state: { _version: 1, name: "Jane Doe" },
migrations,
});
// { _version: 2, firstName: "Jane", lastName: "Doe" }Add a new optional feature to existing data:
const migrations = createMigrations()
.add({
version: 1,
schema: z.object({
email: z.string(),
}),
up: (data) => ({
email: data.email ?? "",
}),
})
.add({
version: 2,
schema: z.object({
email: z.string(),
notifications: z.boolean(),
}),
up: (data) => ({
...data,
notifications: true, // default to enabled
}),
})
.build();Rename a field while preserving its value:
const migrations = createMigrations()
.add({
version: 1,
schema: z.object({ userName: z.string() }),
up: (data) => ({ userName: data.userName ?? "" }),
})
.add({
version: 2,
schema: z.object({ username: z.string() }), // lowercase
up: (data) => ({
username: data.userName,
}),
})
.build();Convert an array of strings to an array of objects:
const migrations = createMigrations()
.add({
version: 1,
schema: z.object({
tags: z.array(z.string()),
}),
up: (data) => ({
tags: Array.isArray(data.tags) ? data.tags : [],
}),
})
.add({
version: 2,
schema: z.object({
tags: z.array(
z.object({
name: z.string(),
color: z.string(),
})
),
}),
up: (data) => ({
tags: data.tags.map((name) => ({
name,
color: "gray", // default color
})),
}),
})
.build();
const result = migrate({
state: { _version: 1, tags: ["work", "urgent"] },
migrations,
});
// {
// _version: 2,
// tags: [
// { name: "work", color: "gray" },
// { name: "urgent", color: "gray" }
// ]
// }Restructure nested data:
const migrations = createMigrations()
.add({
version: 1,
schema: z.object({
street: z.string(),
city: z.string(),
zip: z.string(),
}),
up: (data) => ({
street: data.street ?? "",
city: data.city ?? "",
zip: data.zip ?? "",
}),
})
.add({
version: 2,
schema: z.object({
address: z.object({
street: z.string(),
city: z.string(),
zip: z.string(),
}),
}),
up: (data) => ({
address: {
street: data.street,
city: data.city,
zip: data.zip,
},
}),
})
.build();
const result = migrate({
state: { _version: 1, street: "123 Main St", city: "NYC", zip: "10001" },
migrations,
});
// {
// _version: 2,
// address: { street: "123 Main St", city: "NYC", zip: "10001" }
// }If your data already uses a field like version or schemaVersion:
const migrations = createMigrations()
.add({
version: 1,
schema: z.object({ name: z.string() }),
up: (data) => ({ name: data.name ?? "" }),
})
.add({
version: 2,
schema: z.object({ name: z.string(), active: z.boolean() }),
up: (data) => ({ ...data, active: true }),
})
.build();
// Data uses "version" instead of "_version"
const data = { version: 1, name: "Jane" };
const result = migrate({
state: data,
migrations,
key: "version",
});
// { version: 2, name: "Jane", active: true }Handle data that doesn't have a version field yet:
const migrations = createMigrations()
.add({
version: 1,
schema: z.object({ name: z.string() }),
up: (data) => ({
// Handle both old unversioned format and explicit v0
name: typeof data === "object" && data !== null && "name" in data
? String(data.name)
: "Unknown",
}),
})
.build();
// No _version field - treated as version 0
const legacyData = { name: "Jane" };
const result = migrate({
state: legacyData,
migrations,
});
// { _version: 1, name: "Jane" }When migrations need to perform async operations, use createAsyncMigrations() and migrateAsync():
import { createAsyncMigrations, migrateAsync } from "@nanocollective/json-up";
import { z } from "zod";
const migrations = createAsyncMigrations()
.add({
version: 1,
schema: z.object({ name: z.string() }),
up: (data) => ({ name: data.name ?? "" }), // sync is fine too
})
.add({
version: 2,
schema: z.object({ name: z.string(), key: z.string() }),
up: async (data) => ({
name: data.name,
key: await generateKey(), // async operation
}),
})
.build();
const result = await migrateAsync({
state: { _version: 1, name: "Jane" },
migrations,
});
// { _version: 2, name: "Jane", key: "generated-key-value" }You can freely mix sync and async up() functions in the same chain. Only migrations that actually need async operations need to use async.