core
vercel-labs/json-render
Core schema, catalog, and AI prompt generation for json-render specs.
What is core?
@json-render/core provides the foundation for defining schemas, creating component catalogs, and generating AI prompts for JSON-based UI/video specs. Use it when building the schema and catalog that AI agents will target, or when working with spec streaming and validation.
- Define schemas with `defineSchema` to structure specs and catalogs
- Create catalogs mapping component names to their props and descriptions using `defineCatalog`
- Generate AI system prompts from schemas with optional custom rules
- Stream and compile JSONL spec patches using `createSpecStreamCompiler`
- Resolve dynamic prop expressions ($state, $bindState, $computed, $template, $cond) at render time
- Validate specs and auto-fix common structural issues
How to install core
npx skills add https://github.com/vercel-labs/json-render --skill core- Node.js and npm/yarn
- @json-render/core package installed
- Zod for prop schema definitions (optional but recommended)
How to use core
- 1.Import `defineSchema` and define your spec and catalog structure
- 2.Import `defineCatalog` and map your components with props and descriptions
- 3.Call `catalog.prompt()` to generate AI system prompts
- 4.Use `createSpecStreamCompiler` if handling streaming JSONL responses from AI
- 5.Call `validateSpec` and `autoFixSpec` to ensure spec validity before rendering
- 6.Use `resolvePropValue` or `resolveElementProps` to evaluate dynamic expressions at runtime
Use cases
- Building a component library schema and catalog for AI to generate UI specs
- Streaming AI-generated specs progressively as JSONL patches
- Validating and repairing user-provided or AI-generated specs before rendering
- Creating state-aware dynamic expressions in component props
- Setting up watchers to trigger actions when state values change
- Full-stack developers building json-render applications
- AI agent developers integrating json-render for spec generation
- Teams defining reusable component catalogs and schemas
- Developers implementing custom state management with StateStore
core FAQ
A schema defines the overall structure of specs and catalogs using `defineSchema`. A catalog is a concrete instance created with `defineCatalog` that maps specific component/action names to their prop definitions and descriptions.
Use `{ "$bindState": "/path" }` on the natural value prop (e.g., `value`, `checked`, `pressed`) of form components. This enables both reading from and writing back to state.
Yes. Pass a `promptTemplate` option when calling `defineSchema`, or provide `customRules` when calling `catalog.prompt()`.
Built-in validators include `required`, `email`, `url`, `numeric`, `minLength`, `maxLength`, `min`, `max`, `pattern`, `matches`, `equalTo`, `lessThan`, `greaterThan`, and `requiredIf`. Cross-field validation uses `$state` expressions.
Implement the `StateStore` interface (with `get`, `set`, `update`, `subscribe` methods) or use `createStateStore` for a simple in-memory implementation.
Full instructions (SKILL.md)
Source of truth, from vercel-labs/json-render.
name: core description: Core package for defining schemas, catalogs, and AI prompt generation for json-render. Use when working with @json-render/core, defining schemas, creating catalogs, or building JSON specs for UI/video generation.
@json-render/core
Core package for schema definition, catalog creation, and spec streaming.
Key Concepts
- Schema: Defines the structure of specs and catalogs (use
defineSchema) - Catalog: Maps component/action names to their definitions (use
defineCatalog) - Spec: JSON output from AI that conforms to the schema
- SpecStream: JSONL streaming format for progressive spec building
Defining a Schema
import { defineSchema } from "@json-render/core";
export const schema = defineSchema((s) => ({
spec: s.object({
// Define spec structure
}),
catalog: s.object({
components: s.map({
props: s.zod(),
description: s.string(),
}),
}),
}), {
promptTemplate: myPromptTemplate, // Optional custom AI prompt
});
Creating a Catalog
import { defineCatalog } from "@json-render/core";
import { schema } from "./schema";
import { z } from "zod";
export const catalog = defineCatalog(schema, {
components: {
Button: {
props: z.object({
label: z.string(),
variant: z.enum(["primary", "secondary"]).nullable(),
}),
description: "Clickable button component",
},
},
});
Generating AI Prompts
const systemPrompt = catalog.prompt(); // Uses schema's promptTemplate
const systemPrompt = catalog.prompt({ customRules: ["Rule 1", "Rule 2"] });
SpecStream Utilities
For streaming AI responses (JSONL patches):
import { createSpecStreamCompiler } from "@json-render/core";
const compiler = createSpecStreamCompiler<MySpec>();
// Process streaming chunks
const { result, newPatches } = compiler.push(chunk);
// Get final result
const finalSpec = compiler.getResult();
Dynamic Prop Expressions
Any prop value can be a dynamic expression resolved at render time:
{ "$state": "/state/key" }- reads a value from the state model (one-way read){ "$bindState": "/path" }- two-way binding: reads from state and enables write-back. Use on the natural value prop (value, checked, pressed, etc.) of form components.{ "$bindItem": "field" }- two-way binding to a repeat item field. Use inside repeat scopes.{ "$cond": <condition>, "$then": <value>, "$else": <value> }- evaluates a visibility condition and picks a branch{ "$template": "Hello, ${/user/name}!" }- interpolates${/path}references with state values{ "$computed": "fnName", "args": { "key": <expression> } }- calls a registered function with resolved args
$cond uses the same syntax as visibility conditions ($state, eq, neq, not, arrays for AND). $then and $else can themselves be expressions (recursive).
Components do not use a statePath prop for two-way binding. Instead, use { "$bindState": "/path" } on the natural value prop (e.g. value, checked, pressed).
{
"color": {
"$cond": { "$state": "/activeTab", "eq": "home" },
"$then": "#007AFF",
"$else": "#8E8E93"
},
"label": { "$template": "Welcome, ${/user/name}!" },
"fullName": {
"$computed": "fullName",
"args": {
"first": { "$state": "/form/firstName" },
"last": { "$state": "/form/lastName" }
}
}
}
import { resolvePropValue, resolveElementProps } from "@json-render/core";
const resolved = resolveElementProps(element.props, { stateModel: myState });
State Watchers
Elements can declare a watch field (top-level, sibling of type/props/children) to trigger actions when state values change:
{
"type": "Select",
"props": { "value": { "$bindState": "/form/country" }, "options": ["US", "Canada"] },
"watch": {
"/form/country": { "action": "loadCities", "params": { "country": { "$state": "/form/country" } } }
},
"children": []
}
Watchers only fire on value changes, not on initial render.
Validation
Built-in validation functions: required, email, url, numeric, minLength, maxLength, min, max, pattern, matches, equalTo, lessThan, greaterThan, requiredIf.
Cross-field validation uses $state expressions in args:
import { check } from "@json-render/core";
check.required("Field is required");
check.matches("/form/password", "Passwords must match");
check.lessThan("/form/endDate", "Must be before end date");
check.greaterThan("/form/startDate", "Must be after start date");
check.requiredIf("/form/enableNotifications", "Required when enabled");
User Prompt Builder
Build structured user prompts with optional spec refinement and state context:
import { buildUserPrompt } from "@json-render/core";
// Fresh generation
buildUserPrompt({ prompt: "create a todo app" });
// Refinement with edit modes (default: patch-only)
buildUserPrompt({ prompt: "add a toggle", currentSpec: spec, editModes: ["patch", "merge"] });
// With runtime state
buildUserPrompt({ prompt: "show data", state: { todos: [] } });
Available edit modes: "patch" (RFC 6902 JSON Patch), "merge" (RFC 7396 Merge Patch), "diff" (unified diff).
Spec Validation
Validate spec structure and auto-fix common issues:
import { validateSpec, autoFixSpec } from "@json-render/core";
const { valid, issues } = validateSpec(spec);
// issues include: missing_child, invalid_visible (malformed conditions),
// repeat_without_children, repeat_state_mismatch (statePath not an array in state)
const { spec: fixed, fixDetails } = autoFixSpec(spec);
// fixDetails entries are { message, lossy }. Lossless fixes relocate
// misplaced fields; lossy fixes prune dangling children references.
// In a repair loop, withhold lossy fixes until retries are exhausted:
const attempt = autoFixSpec(spec, { lossy: retriesExhausted });
Visibility Conditions
Control element visibility with state-based conditions. VisibilityContext is { stateModel: StateModel }.
import { visibility } from "@json-render/core";
// Syntax
{ "$state": "/path" } // truthiness
{ "$state": "/path", "not": true } // falsy
{ "$state": "/path", "eq": value } // equality
[ cond1, cond2 ] // implicit AND
// Helpers
visibility.when("/path") // { $state: "/path" }
visibility.unless("/path") // { $state: "/path", not: true }
visibility.eq("/path", val) // { $state: "/path", eq: val }
visibility.and(cond1, cond2) // { $and: [cond1, cond2] }
visibility.or(cond1, cond2) // { $or: [cond1, cond2] }
visibility.always // true
visibility.never // false
Built-in Actions in Schema
Schemas can declare builtInActions -- actions that are always available at runtime and auto-injected into prompts:
const schema = defineSchema(builder, {
builtInActions: [
{ name: "setState", description: "Update a value in the state model" },
],
});
These appear in prompts as [built-in] and don't require handlers in defineRegistry.
StateStore
The StateStore interface allows external state management libraries (Redux, Zustand, XState, etc.) to be plugged into json-render renderers. The createStateStore factory creates a simple in-memory implementation:
import { createStateStore, type StateStore } from "@json-render/core";
const store = createStateStore({ count: 0 });
store.get("/count"); // 0
store.set("/count", 1); // updates and notifies subscribers
store.update({ "/a": 1, "/b": 2 }); // batch update
store.subscribe(() => {
console.log(store.getSnapshot()); // { count: 1 }
});
The StateStore interface: get(path), set(path, value), update(updates), getSnapshot(), subscribe(listener).
Key Exports
| Export | Purpose |
|---|---|
defineSchema | Create a new schema |
defineCatalog | Create a catalog from schema |
createStateStore | Create a framework-agnostic in-memory StateStore |
resolvePropValue | Resolve a single prop expression against data |
resolveElementProps | Resolve all prop expressions in an element |
buildUserPrompt | Build user prompts with refinement and state context |
buildEditUserPrompt | Build user prompt for editing existing specs |
buildEditInstructions | Generate prompt section for available edit modes |
isNonEmptySpec | Check if spec has root and at least one element |
deepMergeSpec | RFC 7396 deep merge (null deletes, arrays replace, objects recurse) |
diffToPatches | Generate RFC 6902 JSON Patch operations from object diff |
EditMode | Type: "patch" | "merge" | "diff" |
validateSpec | Validate spec structure |
autoFixSpec | Auto-fix common spec issues; classifies fixes lossy/lossless, { lossy: false } withholds pruning |
createSpecStreamCompiler | Stream JSONL patches into spec |
createJsonRenderTransform | TransformStream separating text from JSONL in mixed streams |
parseSpecStreamLine | Parse single JSONL line |
applySpecStreamPatch | Apply patch to object |
StateStore | Interface for plugging in external state management |
ComputedFunction | Function signature for $computed expressions |
check | TypeScript helpers for creating validation checks |
BuiltInAction | Type for built-in action definitions (name + description) |
ActionBinding | Action binding type (includes preventDefault field) |
Related skills
More from vercel-labs/json-render and the wider catalog.

devtools
Drop-in inspector panel for any json-render app. Use when the user wants to debug a generative UI, inspect the spec tree, edit state at runtime, see dispatched actions, follow stream patches live, browse a catalog, or pick DOM elements to find their spec keys. Triggers include "add devtools", "debug json-render", "inspect the spec", "why is this element not rendering", "see the state at runtime", or requests to tap streams / capture action logs for `@json-render/devtools`.

image
Image renderer for json-render that turns JSON specs into SVG and PNG images via Satori. Use when working with @json-render/image, generating OG images from JSON, creating social cards, or rendering AI-generated image specs.

ink
Ink terminal renderer for json-render that turns JSON specs into interactive terminal UIs. Use when working with @json-render/ink, building terminal UIs from JSON, creating terminal component catalogs, or rendering AI-generated specs in the terminal.

jotai
Jotai adapter for json-render's StateStore interface. Use when integrating json-render with Jotai for state management via @json-render/jotai.

mcp
MCP Apps integration for json-render. Use when building MCP servers that render interactive UIs in Claude, ChatGPT, Cursor, or VS Code, or when integrating json-render with the Model Context Protocol.

next
Next.js renderer for json-render that turns JSON specs into full Next.js applications with routes, layouts, SSR, and metadata. Use when working with @json-render/next, building Next.js apps from JSON specs, or creating AI-generated multi-page applications.