@oh-just-another/serialization
v0.60.0
Published
Versioned scene (de)serialization with forward migrations and zod validation.
Maintainers
Readme
@oh-just-another/serialization
L2 wire format for @oh-just-another/scene. Validates and round-trips a scene through a versioned JSON document. Pure logic — no DOM, no Node API.
Quick start
import { serializeScene, stringifyScene, parseScene } from "@oh-just-another/serialization";
// Save
localStorage.setItem("scene", stringifyScene(editor.scene));
// Load
const scene = parseScene(localStorage.getItem("scene")!);
editor.loadScene(scene);Wire format
{
"format": "oh-just-another/scene",
"version": 1,
"elements": [...],
"links": [...],
"layers": [...],
"viewport": { "pan": {...}, "zoom": 1, "rotation": 0, "size": {...} }
}format— magic constant, lets a file be recognized without sniffing.version— incremented on any breaking schema change. Migrations live inmigrations.ts.- The element sub-schema is a
z.discriminatedUnionover every built-intype, plus apassthrougharm for plugin-registered types.
API
| Name | Purpose |
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| CURRENT_VERSION, SceneDocument, SceneDocumentZ | Wire-format constants and types. |
| serializeScene(scene) / stringifyScene(scene, indent?) | In-memory Scene → wire document / JSON string. |
| deserializeScene(raw, options?) / parseScene(json, options?) | Wire document / JSON string → typed Scene. Runs migrations + zod validation + brand re-application. |
| DeserializationError | Thrown on validation failure. reason carries the original ZodError. |
| sceneJsonSchema() | JSON Schema (draft-07) of the wire document, generated from the zod schema. For LLM structured output / external validators. |
| registerMigration(fromVersion, fn) / runMigrations(doc, from, to) | Forward migrations between schema versions. |
| MissingMigrationError | Thrown when an intermediate version has no migration registered. |
| serializeFiles(files) / stringifyFiles(files, indent?) | Scene.files binary sidecar → wire document / JSON string. |
| parseFiles(json) | JSON string → typed Scene.files. |
| SerializedFilesDocument | Wire-format type for the file sidecar. |
Design notes
- One
versionfield, forward-only migrations. No back-migrations — exporting always lands atCURRENT_VERSION. Loading an older version walks through every migration in order. - Strict schemas with a
passthrougharm for unknown shapetypes. Plugins that register a custom shape can persist it without modifying the wire schema; the kernel just hands the raw object to the bounder/renderer registry on load. - Deserialise re-brands ids.
ElementId/LinkId/LayerIdare branded strings in the kernel but plain strings in JSON;hydratecasts them back throughelementId()/linkId()/layerId(). exactOptionalPropertyTypesworkaround. Zod's parsed output exposes optional fields asT | undefined, which the kernel rejects. The hydrate path stripsundefined-valued keys before constructing the typed shape.
