@json-render/core
Core library for json-render. Define schemas, create catalogs, generate AI prompts, and stream specs.
Installation
npm install @json-render/core zod
Key Concepts
- Schema: Defines the structure of specs and catalogs
- Catalog: Maps component/action names to their definitions with Zod props
- Spec: JSON output from AI that conforms to the schema
- SpecStream: JSONL streaming format for progressive spec building
Quick Start
Define a Schema
import { defineSchema } from "@json-render/core";
export const schema = defineSchema((s) => ({
spec: s.object({
root: s.object({
type: s.ref("catalog.components"),
props: s.propsOf("catalog.components"),
children: s.array(s.string()),
}),
}),
catalog: s.object({
components: s.map({
props: s.zod(),
description: s.string(),
}),
actions: s.map({
description: s.string(),
}),
}),
}), {
promptTemplate: myPromptTemplate,
});
Create a Catalog
import { defineCatalog } from "@json-render/core";
import { schema } from "./schema";
import { z } from "zod";
export const catalog = defineCatalog(schema, {
components: {
Card: {
props: z.object({
title: z.string(),
subtitle: z.string().nullable(),
}),
description: "A card container with title",
},
Button: {
props: z.object({
label: z.string(),
variant: z.enum(["primary", "secondary"]).nullable(),
}),
description: "A clickable button",
},
},
actions: {
submit: { description: "Submit the form" },
cancel: { description: "Cancel and close" },
},
});
Generate AI Prompts
const systemPrompt = catalog.prompt();
const systemPrompt = catalog.prompt({
system: "You are a dashboard builder.",
customRules: [
"Always include a header",
"Use Card components for grouping",
],
});
Stream AI Responses (SpecStream)
The SpecStream format uses JSONL patches to progressively build specs:
import { createSpecStreamCompiler } from "@json-render/core";
const compiler = createSpecStreamCompiler<MySpec>();
while (streaming) {
const chunk = await reader.read();
const { result, newPatches } = compiler.push(chunk);
if (newPatches.length > 0) {
setSpec(result);
}
}
const finalSpec = compiler.getResult();
SpecStream format uses RFC 6902 JSON Patch operations (each line is a patch):
{"op":"add","path":"/root","value":"card-1"}
{"op":"add","path":"/elements/card-1","value":{"type":"Card","props":{"title":"Hello"},"children":["btn-1"]}}
{"op":"add","path":"/elements/btn-1","value":{"type":"Button","props":{"label":"Click"},"children":[]}}
All six RFC 6902 operations are supported: add, remove, replace, move, copy, test.
Low-Level Utilities
import {
parseSpecStreamLine,
applySpecStreamPatch,
compileSpecStream,
} from "@json-render/core";
const patch = parseSpecStreamLine('{"op":"add","path":"/root","value":{}}');
const obj = {};
applySpecStreamPatch(obj, patch);
const spec = compileSpecStream<MySpec>(jsonlString);
API Reference
Schema
defineSchema(builder, options?) | Create a schema with spec/catalog structure |
SchemaBuilder | Builder with s.object(), s.array(), s.map(), etc. |
Schema options:
promptTemplate | Custom AI prompt generator |
defaultRules | Default rules injected before custom rules in prompts |
builtInActions | Actions always available at runtime, auto-injected into prompts (e.g. setState) |
Catalog
defineCatalog(schema, data) | Create a type-safe catalog from schema |
catalog.prompt(options?) | Generate AI system prompt |
SpecStream
createSpecStreamCompiler<T>() | Create streaming compiler |
parseSpecStreamLine(line) | Parse single JSONL line |
applySpecStreamPatch(obj, patch) | Apply patch to object |
compileSpecStream<T>(jsonl) | Compile entire JSONL string |
Dynamic Props
resolvePropValue(value, ctx) | Resolve a single prop expression |
resolveElementProps(props, ctx) | Resolve all prop expressions in an element |
PropExpression<T> | Type for prop values that may contain expressions |
User Prompt
buildUserPrompt(options) | Build a user prompt with optional spec refinement and state context |
UserPromptOptions | Options type for buildUserPrompt |
Spec Validation
validateSpec(spec, options?) | Validate spec structure and return issues |
autoFixSpec(spec) | Auto-fix common spec issues (returns corrected copy) |
formatSpecIssues(issues) | Format validation issues as readable strings |
Actions
ActionBinding | Action binding with action, params, confirm, preventDefault, etc. |
BuiltInAction | Built-in action definition with name and description |
Chat Mode (Mixed Streams)
createJsonRenderTransform() | TransformStream that separates text from JSONL patches in a mixed stream |
pipeJsonRender() | Server-side helper to pipe a mixed stream through the transform |
SPEC_DATA_PART / SPEC_DATA_PART_TYPE | Constants for filtering spec data parts |
The transform 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.
Types
Spec | Base spec type |
Catalog | Catalog type |
BuiltInAction | Built-in action type (name + description) |
VisibilityCondition | Visibility condition type (used by $cond) |
VisibilityContext | Context for evaluating visibility and prop expressions |
SpecStreamLine | Single patch operation |
SpecStreamCompiler | Streaming compiler interface |
Dynamic Prop Expressions
Any prop value can be a dynamic expression that resolves based on data state at render time. Expressions are resolved by the renderer before props reach components.
Data Binding ($state)
Read a value directly from the state model:
{
"color": { "$state": "/theme/primary" },
"label": { "$state": "/user/name" }
}
Two-Way Binding ($bindState / $bindItem)
Use { "$bindState": "/path" } on the natural value prop for form components that need read/write access. The component reads from and writes to the state path:
{
"type": "Input",
"props": {
"value": { "$bindState": "/form/email" },
"placeholder": "Email"
}
}
Inside a repeat scope, use { "$bindItem": "completed" } to bind to a field on the current item:
Conditional ($cond / $then / $else)
Evaluate a condition (same syntax as visibility conditions) and pick a value:
{
"color": {
"$cond": { "$state": "/activeTab", "eq": "home" },
"$then": "#007AFF",
"$else": "#8E8E93"
},
"name": {
"$cond": { "$state": "/activeTab", "eq": "home" },
"$then": "home",
"$else": "home-outline"
}
}
$then and $else can themselves be expressions (recursive):
{
"label": {
"$cond": { "$state": "/user/isAdmin" },
"$then": { "$state": "/admin/greeting" },
"$else": "Welcome"
}
}
Repeat Item ($item)
Inside children of a repeated element, read a field from the current array item:
{ "$item": "title" }
Use "" to get the entire item object. $item takes a path string because items are typically objects with nested fields to navigate.
Repeat Index ($index)
Get the current array index inside a repeat:
{ "$index": true }
$index uses true as a sentinel flag because the index is a scalar value with no sub-path to navigate (unlike $item which needs a path).
API
import { resolvePropValue, resolveElementProps } from "@json-render/core";
const color = resolvePropValue(
{ $cond: { $state: "/active", eq: "yes" }, $then: "blue", $else: "gray" },
{ stateModel: myState }
);
const resolved = resolveElementProps(element.props, { stateModel: myState });
Visibility Conditions
Visibility conditions control when elements are shown. VisibilityContext is { stateModel: StateModel, repeatItem?: unknown, repeatIndex?: number }.
Syntax
{ "$state": "/path" }
{ "$state": "/path", "not": true }
{ "$state": "/path", "eq": value }
{ "$state": "/path", "neq": value }
{ "$state": "/path", "gt": number }
{ "$item": "field" }
{ "$index": true, "gt": 0 }
[ condition, condition ]
{ "$and": [ condition, condition ] }
{ "$or": [ condition, condition ] }
true / false
TypeScript Helpers
import { visibility } from "@json-render/core";
visibility.always
visibility.never
visibility.when("/path")
visibility.unless("/path")
visibility.eq("/path", val)
visibility.neq("/path", val)
visibility.gt("/path", n)
visibility.gte("/path", n)
visibility.lt("/path", n)
visibility.lte("/path", n)
visibility.and(cond1, cond2)
visibility.or(cond1, cond2)
User Prompt Builder
Build structured user prompts for AI generation, with support for refinement and state context:
import { buildUserPrompt } from "@json-render/core";
const prompt = buildUserPrompt({ prompt: "create a todo app" });
const refinementPrompt = buildUserPrompt({
prompt: "add a dark mode toggle",
currentSpec: existingSpec,
});
const contextPrompt = buildUserPrompt({
prompt: "show my data",
state: { todos: [{ text: "Buy milk" }] },
});
Spec Validation
Validate spec structure and auto-fix common issues:
import { validateSpec, autoFixSpec, formatSpecIssues } from "@json-render/core";
const { valid, issues } = validateSpec(spec);
console.log(formatSpecIssues(issues));
const fixed = autoFixSpec(spec);
Custom Schemas
json-render supports completely different spec formats for different renderers:
{ root: "card-1", elements: { "card-1": { type: "Card", props: {...}, children: [...] } } }
{ composition: {...}, tracks: [...], clips: [...] }
{ pages: [...], navigation: {...}, theme: {...} }
Each renderer defines its own schema with defineSchema() and its own prompt template.