@json-render/core
Core types, schemas, and utilities.
defineCatalog
Creates a type-safe catalog definition with schema validation.
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/react';
function defineCatalog<T extends ZodType>(
s: T,
config: CatalogConfig
): Catalog
// Use the React schema for standard UI specs
const catalog = defineCatalog(schema, {
components: {...},
actions: {...},
});CatalogConfig
interface CatalogConfig {
components: Record<string, ComponentDefinition>;
actions?: Record<string, ActionDefinition>;
functions?: Record<string, FunctionDefinition>;
}
interface ComponentDefinition {
props: ZodObject; // Use .nullable() for optional props
slots?: string[]; // Named slots (e.g., ["default"])
description?: string; // Help AI understand usage
}
interface ActionDefinition {
params?: ZodObject;
description?: string;
}
interface FunctionDefinition {
description?: string;
}Catalog Instance
The returned catalog provides methods for AI prompt generation, validation, and schema export:
interface Catalog {
// Data
readonly data: CatalogConfig; // The catalog configuration
readonly componentNames: string[]; // List of component names
readonly actionNames: string[]; // List of action names
// AI Prompt Generation
prompt(options?: PromptOptions): string;
// Validation
validate(spec: unknown): SpecValidationResult;
zodSchema(): z.ZodType; // Get the Zod schema for specs
// Export
jsonSchema(): object; // Export as JSON Schema
}
interface PromptOptions {
system?: string; // Custom system message intro
customRules?: string[]; // Additional rules to append
mode?: "generate" | "chat"; // Output mode (default: "generate")
}
interface SpecValidationResult<T> {
success: boolean;
data?: T; // Validated spec (if success)
error?: z.ZodError; // Validation errors (if failed)
}Catalog Methods
// Generate AI system prompt
const systemPrompt = catalog.prompt({
customRules: ["Always use Card as root element"],
});
// Validate a spec from AI
const result = catalog.validate(aiOutput);
if (result.success) {
render(result.data);
} else {
console.error(result.error);
}
// Get Zod schema for custom validation
const schema = catalog.zodSchema();
const parsed = schema.safeParse(aiOutput);
// Export as JSON Schema (for structured outputs)
const jsonSchema = catalog.jsonSchema();Schema System
json-render uses a flexible schema system that defines both the AI output format (spec) and what catalogs must provide. Each renderer package provides its own schema (e.g., @json-render/react exports schema).
schema
The schema for flat UI element trees. This is exported from @json-render/react.
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/react';
// schema defines:
// - Spec shape: { root: string, elements: Record<string, UIElement> }
// - Catalog shape: { components: {...}, actions: {...} }
const catalog = defineCatalog(schema, {
components: {
Card: {
props: z.object({ title: z.string() }),
slots: ["default"],
description: "Container card",
},
},
actions: {
submit: {
params: z.object({ formId: z.string() }),
description: "Submit a form",
},
},
});SchemaOptions
When creating schemas with defineSchema, you can pass options:
interface SchemaOptions {
promptTemplate?: PromptTemplate; // Custom AI prompt generator
defaultRules?: string[]; // Default rules injected before custom rules in prompts
builtInActions?: BuiltInAction[]; // Actions always available at runtime, auto-injected into prompts
}
interface BuiltInAction {
name: string; // Action name (e.g. "setState")
description: string; // Human-readable description for the LLM
}Built-in actions are injected into prompts as [built-in] and are handled by the runtime (e.g. ActionProvider) without requiring handlers in defineRegistry. The React schema declares setState, pushState, and removeState as built-in.
defineSchema
Create custom schemas for different output formats (e.g., page-based, block-based).
import { defineSchema } from '@json-render/core';
const mySchema = defineSchema((s) => ({
// What the AI outputs (spec)
spec: s.object({
title: s.string(),
blocks: s.array(s.object({
type: s.ref("catalog.blocks"),
content: s.any(),
})),
}),
// What the catalog must provide
catalog: s.object({
blocks: s.map({
props: s.zod(),
description: s.string(),
}),
}),
}));Schema Builder API
The schema builder provides these methods:
// Primitive types
s.string() // String value
s.number() // Number value
s.boolean() // Boolean value
s.any() // Any value
// Compound types
s.array(item) // Array of items
s.object({ ... }) // Object with shape
s.record(value) // Record/map with value type
// Catalog references (for type safety)
s.ref("catalog.components") // Reference to catalog key (becomes enum)
s.propsOf("catalog.components") // Props schema from catalog entry
// Catalog definitions
s.map({ props: s.zod(), ... }) // Map of named entries with shared shape
s.zod() // Placeholder for user-provided Zod schema
// Modifiers
s.optional() // Mark field as optionalZod Schemas
Pre-built Zod schemas for common json-render types:
Dynamic Value Schemas
import {
DynamicValueSchema, // string | number | boolean | null | { $state: string }
DynamicStringSchema, // string | { $state: string }
DynamicNumberSchema, // number | { $state: string }
DynamicBooleanSchema, // boolean | { $state: string }
} from '@json-render/core';
// Dynamic values can be literals or state path references
type DynamicValue<T> = T | { $state: string };
// Example: a prop that can be a literal or bound to state
const schema = z.object({
label: DynamicStringSchema, // "Hello" or { $state: "/user/name" }
});Visibility Schemas
import { VisibilityConditionSchema } from '@json-render/core';
// Use in component props that need conditional rendering
const schema = z.object({
visible: VisibilityConditionSchema.optional(),
});Action Schemas
import {
ActionSchema, // Full action definition
ActionConfirmSchema, // Confirmation dialog config
ActionOnSuccessSchema, // Success handler config
ActionOnErrorSchema, // Error handler config
} from '@json-render/core';Validation Schemas
import {
ValidationCheckSchema, // Single validation check
ValidationConfigSchema, // Full validation config with checks array
} from '@json-render/core';SpecStream
SpecStream is json-render's streaming format for progressively building specs from JSONL patches.
createSpecStreamCompiler
Create a streaming compiler that incrementally builds a spec:
import { createSpecStreamCompiler } from '@json-render/core';
const compiler = createSpecStreamCompiler<MySpec>();
// Process streaming chunks
const { result, newPatches } = compiler.push(chunk);
// Get final result
const spec = compiler.getResult();
// Reset for reuse
compiler.reset();compileSpecStream
Compile an entire SpecStream string at once:
import { compileSpecStream } from '@json-render/core';
const jsonl = `{"op":"add","path":"/root","value":{}}
{"op":"add","path":"/root/type","value":"Card"}`;
const spec = compileSpecStream<MySpec>(jsonl);Low-Level Utilities
import {
parseSpecStreamLine,
applySpecStreamPatch,
} from '@json-render/core';
// Parse a single line
const patch = parseSpecStreamLine('{"op":"add","path":"/root","value":{}}');
// Apply patch to object (mutates in place)
const obj = {};
applySpecStreamPatch(obj, patch);applySpecPatch
Apply a single SpecStream patch to a Spec object (mutates in place, returns the spec):
import { applySpecPatch } from '@json-render/core';
let spec: Spec = { root: "", elements: {} };
applySpecPatch(spec, { op: "add", path: "/root", value: "main" });
// For React state updates, spread to create a new reference:
setSpec({ ...applySpecPatch(spec, patch) });nestedToFlat
Convert a nested element tree (with inline children) into the flat Spec format:
import { nestedToFlat } from '@json-render/core';
const flat = nestedToFlat({
type: "Card",
props: { title: "Hello" },
children: [
{ type: "Text", props: { content: "World" }, children: [] }
],
});
// { root: "el-0", elements: { "el-0": ..., "el-1": ... } }createJsonRenderTransform
Low-level TransformStream that separates text from JSONL patches in a mixed AI stream. Lines that parse as JSONL patches are emitted as data-spec parts; everything else passes through as text.
The transform properly splits text blocks around spec data by emitting text-end/text-start pairs, ensuring the AI SDK creates separate text parts and preserving correct interleaving of prose and UI in message.parts.
import { createJsonRenderTransform } from '@json-render/core';
const transform = createJsonRenderTransform();
// Use with ReadableStream.pipeThrough(transform) for custom pipelinesMost users should use pipeJsonRender() instead, which wraps this transform for the common AI SDK use case.
createMixedStreamParser
Parse a mixed stream of text and JSONL patches (used for Chat + GenUI mode):
import { createMixedStreamParser } from '@json-render/core';
const parser = createMixedStreamParser({
onText: (text) => appendToMessage(text),
onPatch: (patch) => applySpecPatch(spec, patch),
});
// As chunks arrive from the stream:
for await (const chunk of stream) {
parser.push(chunk);
}
parser.flush();pipeJsonRender
Pipe an AI SDK UIMessageStream through the json-render transform. Lines that parse as JSONL patches are emitted as data-spec parts; everything else passes through as text. Used in Chat mode API routes.
import { pipeJsonRender } from '@json-render/core';
import { createUIMessageStream, createUIMessageStreamResponse } from 'ai';
const stream = createUIMessageStream({
execute: async ({ writer }) => {
writer.merge(pipeJsonRender(result.toUIMessageStream()));
},
});
return createUIMessageStreamResponse({ stream });See Generation Modes for full Chat mode setup.
SpecStream Types
Fully compliant with RFC 6902:
interface SpecStreamLine {
op: 'add' | 'remove' | 'replace' | 'move' | 'copy' | 'test';
path: string;
value?: unknown; // Required for add, replace, test
from?: string; // Required for move, copy
}
interface SpecStreamCompiler<T> {
push(chunk: string): { result: T; newPatches: SpecStreamLine[] };
getResult(): T;
getPatches(): SpecStreamLine[];
reset(): void;
}
interface MixedStreamCallbacks {
onText: (text: string) => void;
onPatch: (patch: SpecStreamLine) => void;
}
interface MixedStreamParser {
push(chunk: string): void;
flush(): void;
}Utility Functions
Path Utilities
import { getByPath, setByPath } from '@json-render/core';
// Get value by JSON Pointer path
const value = getByPath(state, '/user/name'); // "Alice"
// Set value by path (mutates object)
setByPath(state, '/user/email', 'alice@example.com');resolveDynamicValue
import { resolveDynamicValue } from '@json-render/core';
// Resolve a dynamic value against state
const name = resolveDynamicValue("Hello", state); // "Hello"
const name2 = resolveDynamicValue({ $state: "/user/name" }, state); // "Alice"findFormValue
import { findFormValue } from '@json-render/core';
// Find form values regardless of path format
// Checks: params.name, params["form.name"], state["form.name"], state.form.name
const value = findFormValue("name", params, state);buildUserPrompt
Build structured user prompts for AI generation, with support for refinement and state context.
import { buildUserPrompt } from '@json-render/core';
function buildUserPrompt(options: UserPromptOptions): string
interface UserPromptOptions {
prompt: string; // The user's text prompt
currentSpec?: Spec | null; // Existing spec to refine (triggers patch-only mode)
state?: Record<string, unknown> | null; // Runtime state context to include
maxPromptLength?: number; // Max length for user text (truncates before wrapping)
}Fresh generation
const userPrompt = buildUserPrompt({ prompt: "create a todo app" });Refinement (patch-only mode)
When currentSpec is provided, the prompt instructs the AI to output only the patches needed for the change, not recreate the entire spec:
const userPrompt = buildUserPrompt({
prompt: "add a dark mode toggle",
currentSpec: existingSpec,
});With state context
Include runtime state so the AI knows what data is available:
const userPrompt = buildUserPrompt({
prompt: "show my data",
state: { todos: [{ text: "Buy milk" }] },
});evaluateVisibility
Evaluates a visibility condition against the state model.
function evaluateVisibility(
condition: VisibilityCondition | undefined,
ctx: VisibilityContext
): boolean
interface VisibilityContext {
stateModel: StateModel;
repeatItem?: unknown; // Current repeat item (inside repeat scope)
repeatIndex?: number; // Current repeat array index (inside repeat scope)
}
type VisibilityCondition =
| { $state: string } // truthiness
| { $state: string; not: true } // falsy
| { $state: string; eq: unknown } // equality
| { $state: string; neq: unknown } // inequality
| { $state: string; gt: number } // greater than
| { $state: string; gte: number } // gte
| { $state: string; lt: number } // lt
| { $state: string; lte: number } // lte
| { $item: string } // item field (repeat scope)
| { $item: string; eq: unknown } // item field equality
| { $index: true } // index truthiness (repeat scope)
| { $index: true; gt: number } // index comparison
| VisibilityCondition[] // implicit AND
| { $and: VisibilityCondition[] } // explicit AND
| { $or: VisibilityCondition[] } // OR
| boolean; // always / neverTypes
UIElement
interface UIElement {
type: string;
props: Record<string, unknown>;
children?: string[]; // Keys of child elements
visible?: VisibilityCondition;
on?: Record<string, ActionBinding | ActionBinding[]>; // Event bindings
repeat?: { statePath: string; key?: string }; // Repeat for arrays
}Elements are stored in the elements map keyed by string IDs. The key comes from the map, not from the element itself.
Spec (Element Tree)
interface Spec {
root: string | null; // Key of root element
elements: Record<string, UIElement>; // Flat element map
state?: Record<string, unknown>; // Initial state model
}Elements are stored as a flat map with string keys. The tree structure is built by following the children arrays.
ActionBinding
interface ActionBinding {
action: string;
params?: Record<string, DynamicValue>;
confirm?: {
title: string;
message: string;
variant?: 'default' | 'danger';
};
onSuccess?: { set: Record<string, unknown> };
onError?: { set: Record<string, unknown> };
preventDefault?: boolean; // Prevent default browser behavior (e.g. navigation on links)
}ValidationSchema
interface ValidationSchema {
checks: ValidationCheck[];
validateOn?: 'change' | 'blur' | 'submit';
}
interface ValidationCheck {
type: string;
args?: Record<string, unknown>;
message: string;
}