Portable JSON UI document specification for Constructive Blocks: the storage format, runtime validators, JSON Schema export, and pure document-manipulation API. No React — this package is safe in servers, workers, and agents.
pnpm add blocks-schemaThe package is built with makage and published from dist, so every module is
a root-level entry point and deep imports need no exports map:
import { parseDocument } from 'blocks-schema';
import { composeDocument } from 'blocks-schema/compose';
import { validateField } from 'blocks-schema/validation';| Layer | Purpose | File |
|---|---|---|
| Envelope | UIDocument container (formatVersion, id, page) |
envelope.ts |
| Nodes | Recursive UINode tree, known node type sets |
node.ts |
| Validation | Zod parsers for documents and nodes | zod.ts |
| JSON Schema | Exported JSON Schema of the format (for agents/tooling) | json-schema.ts |
| Compose | Pure fragment/slot/override composition | compose.ts |
| Fields | Field collection helpers and constraint validation | node.ts, validation.ts |
Rendering lives in adapters — blocks-renderer is the React
adapter over this spec.
{
"formatVersion": "1.0",
"type": "UISchema",
"id": "orders-form",
"meta": { "title": "Orders" },
"page": { "type": "Page", "key": "page", "props": {}, "children": [] }
}interface UINode {
type: string; // Known block type or any registry-resolved type
key: string; // Unique within the document; identity for overrides
props: UINodeProps; // Static props (label, name, defaultValue, constraints, ...)
children: UINode[];
bindings?: Record<string, string>; // prop → "{{ scope.path }}" template
actions?: Record<string, UIAction>; // event → flow/handler action
}Unknown node types are valid documents — resolution happens at render time, and adapters must render a visible fallback rather than throw.
keymust be unique within a document; it is the node's identity for overrides, slots, and tooling.- Documents are plain declarative JSON: no expressions beyond
{{ path }}bindings, no embedded code. Fragmentnodes reference reusable subtrees viaprops.ref.Slotnodes declare named insertion points viaprops.name; their children are the default content when no filler is supplied.- Composition is pure and never mutates its input.
import {
parseDocument, safeParseDocument, isUIDocument, // validation
toDocumentJsonSchema, toNodeJsonSchema, // JSON Schema export
composeDocument, // fragments/slots/overrides
collectFieldNames, collectDefaultValues,
collectFieldConstraints, validateField, // form helpers
walkNodes, findNodeByKey,
} from 'blocks-schema';
const composed = composeDocument(document, {
fragments: { address: addressSubtree },
slots: { header: customHeaderNode },
overrides: { title: { props: { label: 'Headline' } }, legacy: { remove: true } },
});