Schemas
Schemas use Zod to define each record's fields. They validate data when it is:
- Loaded from the local cache.
- Changed by application code.
- Received from the server.
- Returned in a server response.
Define accounts and collections
import { defineAccount, defineCollection, type infer } from "valtio-sync/schema";
import { z } from "zod";
const account = defineAccount({
fields: {
theme: z.enum(["light", "dark"]).default("light"),
},
});
const todos = defineCollection({
fields: {
id: z.string(),
title: z.string().default(""),
completed: z.boolean().default(false),
},
});
type Todo = infer<typeof todos>;
type TodoFromZod = z.infer<typeof todos.recordSchema>; // Same as Todo
Reuse the record schema
Each definition exposes its Zod schema as recordSchema. It includes field defaults and transforms, and rejects undeclared fields.
Use it when you need to validate the same record elsewhere. You do not need a separate Zod object:
export const TodoRecord = todos.recordSchema;
export type TodoRecord = infer<typeof todos>;
Validate rules across fields
Use the definition-level refine callback for rules involving multiple fields. It has
the same record and issue context as Zod's superRefine:
const account = defineAccount({
fields: {
maxPinnedTodos: z.number().int().nonnegative(),
pinnedTodoIds: z.array(z.string()),
},
refine: (record, ctx) => {
if (record.pinnedTodoIds.length > record.maxPinnedTodos) {
ctx.addIssue({
code: 'custom',
path: ['pinnedTodoIds'],
message: 'Pinned todos exceed the configured limit',
})
}
},
})
Assemble the sync schema
Use exactly one account definition in each sync schema:
const schema = { account, todos }
Collections should include an id: z.string() field. The collection API creates records by id, and strict schema validation rejects values with fields that are not declared.
Set defaults
Defaults are applied when records are created and when saved local data is loaded:
const todo = sync.todos.create({ id: 'todo_1' })
todo.title // ""
todo.completed // false
Use JSON-compatible values
Synced records and patches must be plain objects that can be serialized as JSON. Avoid:
Date,Map, andSet.- Class instances and functions.
undefined,NaN, and infinite numbers.
Store encoded strings or plain objects instead.
Define local-only state
Local-only state uses the same field-map shape, but it is passed directly to the client as device or session fields rather than through defineAccount or defineCollection.