Schema serialization: runtime ↔ serialized round-trip
At a glance
- Converts a schema (live Zod validators + live functions) to a JSON-safe form and back —
serializeSchema/deserializeSchema. - Only 7 Zod shapes survive the round-trip (
string,number,boolean,any,enum,literal,array) — anything else errors, it doesn't silently drop detail. - Functions serialize as their own source text, and
toString()is patched to keep returning that same text after deserializing. - All conversions return a
neverthrowResult, never throw.
A schema here is not the tree/entry model from Tree nodes and schemas — it's one level up: the definition of what's editable for a given file type, including live Zod validators and live JavaScript functions (prefill, mutate). Neither Zod schemas nor functions survive JSON.stringify, so anything that needs to store or transmit a schema (the database, an API response, a template's saved config) needs a JSON-safe stand-in it can convert back later. That's what this package does: convert a RuntimeSchema to a SerializedSchema and back.
Runtime vs. Serialized
Every shape in this package comes in two parallel forms:
Runtime (types/runtime.ts) | Serialized (types/definition.ts, types/config.ts) |
|---|---|
RuntimeSchema | SerializedSchema |
RuntimeSchemaMap / RuntimeSchemaEntry | SerializedSchemaMap / SerializedSchemaEntry |
RuntimeSchemaConfig (sections of admin-configurable options, each with a live mutate function) | SerializedSchemaConfig |
The two are structurally identical except wherever the runtime side holds a live Zod type or a live function, the serialized side holds a descriptor instead:
- Zod validators become a
ZodDescriptor— a small tagged union ({ zod: 'string' },{ zod: 'number' },{ zod: 'boolean' },{ zod: 'any' },{ zod: 'enum', values },{ zod: 'literal', value }, or{ zod: 'array', items }recursively). This is a closed set —serializeZodonly recognizes those seven shapes (zod-guards.tschecks each one byinstanceofor by reading_def.typeName, so it also works across separately-loaded Zod instances). A validator built from anything else (z.object,z.union,z.record, ...) makesserializeZodreturn aZodSerializationErrorrather than silently dropping detail. - Functions (
prefillon an entry,mutateon a config section) become anFnDescriptor— literally{ body: string }, the function's own.toString()output. (FnDescriptoritself isn't re-exported from the package entry point, so it has no Reference page — it only shows up as the type ofprefill/mutateon the entry/section types above.) Deserializing runs that string back through theFunction(...)constructor.deserializeFnthen patches the rebuilt function'stoString(viawithSource) to return the original body text — so if you serialize a function, deserialize it, and serialize it again, you get the same source string back rather than V8's genericfunction anonymous() { [native code] }wrapper.
The conversion functions
serializeSchema and deserializeSchema are the entry points; both return a neverthrow Result<T, ParserError> rather than throwing, so callers handle failure explicitly instead of wrapping every call in try/catch. Internally each walks the schema recursively — map → its values (entries or nested maps) → each entry's validation/prefill — mirroring the recursive shape of RuntimeSchemaMap/SerializedSchemaMap itself. deserializeSchema additionally runs the input through serializedSchemaSchema.safeParse first (a real Zod schema describing the serialized shape) — so malformed input coming from outside the app (a hand-edited database row, a bad API payload) is rejected before any recursive walking starts, rather than throwing partway through.
ParserError has three cases: ZodDeserializationError, ZodSerializationError, FunctionDeserializationError — errors are categorized by which side of the conversion failed, not by which field.
Where this fits
A RuntimeSchema's config section is what TemplateSave.config snapshots (configSnapshot) come from, and a schema's definition is the template form of the tree model described elsewhere.
Extending the Zod descriptor set
If you're adding a new kind of value a schema's Zod validators need to express, it has to be added to the closed set in zod-serialize.ts / zod-deserialize.ts / zod-guards.ts together — all three, or serialization will start failing for that field with a ZodSerializationError rather than a helpful compile error.