
Security News
The AI Industry Is Betting on Open Weights
An open letter signed by 50 companies, from NVIDIA and Microsoft to Mistral and Hugging Face, urges Washington not to restrict open weight AI.
@json-render/core
Advanced tools
JSON becomes real things. Define your catalog, register your components, let AI generate.
Core library for json-render. Define schemas, create catalogs, generate AI prompts, and stream specs.
npm install @json-render/core zod
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()), // Element keys (flat spec format)
}),
}),
catalog: s.object({
components: s.map({
props: s.zod(),
description: s.string(),
}),
actions: s.map({
description: s.string(),
}),
}),
}), {
promptTemplate: myPromptTemplate, // Optional custom AI prompt generator
});
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 system prompt for AI
const systemPrompt = catalog.prompt();
// With custom rules
const systemPrompt = catalog.prompt({
system: "You are a dashboard builder.",
customRules: [
"Always include a header",
"Use Card components for grouping",
],
});
The SpecStream format uses JSONL patches to progressively build specs:
import { createSpecStreamCompiler } from "@json-render/core";
// Create a compiler for your spec type
const compiler = createSpecStreamCompiler<MySpec>();
// Process streaming chunks from AI
while (streaming) {
const chunk = await reader.read();
const { result, newPatches } = compiler.push(chunk);
if (newPatches.length > 0) {
// Update UI with partial result
setSpec(result);
}
}
// Get final compiled 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.
import {
parseSpecStreamLine,
applySpecStreamPatch,
compileSpecStream,
} from "@json-render/core";
// Parse a single line
const patch = parseSpecStreamLine('{"op":"add","path":"/root","value":{}}');
// { op: "add", path: "/root", value: {} }
// Apply a patch to an object
const obj = {};
applySpecStreamPatch(obj, patch);
// obj is now { root: {} }
// Compile entire JSONL string at once
const spec = compileSpecStream<MySpec>(jsonlString);
| Export | Purpose |
|---|---|
defineSchema(builder, options?) | Create a schema with spec/catalog structure |
SchemaBuilder | Builder with s.object(), s.array(), s.map(), etc. |
Schema options:
| Option | Purpose |
|---|---|
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) |
| Export | Purpose |
|---|---|
defineCatalog(schema, data) | Create a type-safe catalog from schema |
catalog.prompt(options?) | Generate AI system prompt |
| Export | Purpose |
|---|---|
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 |
| Export | Purpose |
|---|---|
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 |
ComputedFunction | Function signature for $computed expressions |
PropResolutionContext | Context for resolving props (includes functions for $computed) |
| Export | Purpose |
|---|---|
check.required() | Required validation helper |
check.email() | Email validation helper |
check.matches(path) | Cross-field match helper |
check.equalTo(path) | Cross-field equality helper |
check.lessThan(path) | Cross-field less-than helper |
check.greaterThan(path) | Cross-field greater-than helper |
check.requiredIf(path) | Conditional required helper |
builtInValidationFunctions | All built-in validation functions |
runValidationCheck() | Run a single validation check |
| Export | Purpose |
|---|---|
buildUserPrompt(options) | Build a user prompt with optional spec refinement and state context |
buildEditUserPrompt(options) | Build a user prompt for editing an existing spec (used internally by buildUserPrompt) |
buildEditInstructions(config, format) | Generate the prompt section describing available edit modes |
isNonEmptySpec(spec) | Check whether a spec has a root and at least one element |
UserPromptOptions | Options type for buildUserPrompt |
EditMode | "patch" | "merge" | "diff" |
EditConfig | Configuration for edit modes ({ modes: EditMode[] }) |
BuildEditUserPromptOptions | Options type for buildEditUserPrompt |
| Export | Purpose |
|---|---|
deepMergeSpec(base, patch) | RFC 7396 deep merge (null deletes, arrays replace, objects recurse) |
diffToPatches(oldObj, newObj) | Generate RFC 6902 JSON Patch operations from object diff |
| Export | Purpose |
|---|---|
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 |
| Export | Purpose |
|---|---|
ActionBinding | Action binding with action, params, confirm, preventDefault, etc. |
BuiltInAction | Built-in action definition with name and description |
| Export | Purpose |
|---|---|
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.
| Export | Purpose |
|---|---|
createStateStore(initialState?) | Create a framework-agnostic in-memory StateStore |
StateStore | Interface for plugging in external state management (Redux, Zustand, XState, etc.) |
StateModel | State model type (Record<string, unknown>) |
The StateStore interface allows renderers to use external state management instead of the built-in internal store:
import { createStateStore, type StateStore } from "@json-render/core";
// Simple in-memory store
const store = createStateStore({ count: 0 });
store.get("/count"); // 0
store.set("/count", 1); // updates and notifies subscribers
store.getSnapshot(); // { count: 1 }
// Subscribe to changes (compatible with React's useSyncExternalStore)
const unsubscribe = store.subscribe(() => {
console.log("state changed:", store.getSnapshot());
});
Pass the store to StateProvider in any renderer package (@json-render/react, @json-render/react-native, @json-render/react-pdf) for controlled mode.
Available via @json-render/core/store-utils:
| Export | Purpose |
|---|---|
createStoreAdapter(config) | Build a full StateStore from a minimal { getSnapshot, setSnapshot, subscribe } config |
immutableSetByPath(root, path, value) | Immutably set a value at a JSON Pointer path with structural sharing |
flattenToPointers(obj) | Flatten a nested object into JSON Pointer keyed entries |
StoreAdapterConfig | Config type for createStoreAdapter |
import { createStoreAdapter, immutableSetByPath, flattenToPointers } from "@json-render/core/store-utils";
createStoreAdapter handles get, set (with no-op detection), batched update, getSnapshot, getServerSnapshot, and subscribe -- adapter authors only need to supply the snapshot source, write API, and subscribe mechanism:
import { createStoreAdapter } from "@json-render/core/store-utils";
const store = createStoreAdapter({
getSnapshot: () => myLib.getState(),
setSnapshot: (next) => myLib.setState(next),
subscribe: (listener) => myLib.subscribe(listener),
});
The official adapter packages (@json-render/redux, @json-render/zustand, @json-render/jotai) are all built on top of createStoreAdapter.
| Export | Purpose |
|---|---|
Spec | Base spec type |
Catalog | Catalog type |
BuiltInAction | Built-in action type (name + description) |
ComputedFunction | Function signature for $computed expressions |
VisibilityCondition | Visibility condition type (used by $cond) |
VisibilityContext | Context for evaluating visibility and prop expressions |
SpecStreamLine | Single patch operation |
SpecStreamCompiler | Streaming compiler interface |
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.
$state)Read a value directly from the state model:
{
"color": { "$state": "/theme/primary" },
"label": { "$state": "/user/name" }
}
$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:
$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"
}
}
$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.
$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).
$template)Interpolate state values into strings using ${/path} syntax:
{
"label": { "$template": "Hello, ${/user/name}! You have ${/inbox/count} messages." }
}
Missing paths resolve to an empty string.
$computed)Call a registered function with resolved arguments:
{
"text": {
"$computed": "fullName",
"args": {
"first": { "$state": "/form/firstName" },
"last": { "$state": "/form/lastName" }
}
}
}
Functions are registered in the catalog and provided at runtime via the functions prop on the renderer.
import type { ComputedFunction } from "@json-render/core";
const functions: Record<string, ComputedFunction> = {
fullName: (args) => `${args.first} ${args.last}`,
};
import { resolvePropValue, resolveElementProps } from "@json-render/core";
// Resolve a single value
const color = resolvePropValue(
{ $cond: { $state: "/active", eq: "yes" }, $then: "blue", $else: "gray" },
{ stateModel: myState }
);
// Resolve all props on an element
const resolved = resolveElementProps(element.props, { stateModel: myState });
Visibility conditions control when elements are shown. VisibilityContext is { stateModel: StateModel, repeatItem?: unknown, repeatIndex?: number }.
{ "$state": "/path" } // truthiness
{ "$state": "/path", "not": true } // falsy
{ "$state": "/path", "eq": value } // equality
{ "$state": "/path", "neq": value } // inequality
{ "$state": "/path", "gt": number } // greater than
{ "$item": "field" } // repeat item field
{ "$index": true, "gt": 0 } // repeat index
[ condition, condition ] // implicit AND
{ "$and": [ condition, condition ] } // explicit AND
{ "$or": [ condition, condition ] } // OR
true / false // always / never
import { visibility } from "@json-render/core";
visibility.always // true
visibility.never // false
visibility.when("/path") // { $state: "/path" }
visibility.unless("/path") // { $state: "/path", not: true }
visibility.eq("/path", val) // { $state: "/path", eq: val }
visibility.neq("/path", val) // { $state: "/path", neq: val }
visibility.gt("/path", n) // { $state: "/path", gt: n }
visibility.gte("/path", n) // { $state: "/path", gte: n }
visibility.lt("/path", n) // { $state: "/path", lt: n }
visibility.lte("/path", n) // { $state: "/path", lte: n }
visibility.and(cond1, cond2) // { $and: [cond1, cond2] }
visibility.or(cond1, cond2) // { $or: [cond1, cond2] }
Build structured user prompts for AI generation, with support for refinement and state context:
import { buildUserPrompt } from "@json-render/core";
// Fresh generation
const prompt = buildUserPrompt({ prompt: "create a todo app" });
// Refinement with edit modes (default: patch-only)
const refinementPrompt = buildUserPrompt({
prompt: "add a dark mode toggle",
currentSpec: existingSpec,
editModes: ["patch", "merge"],
});
// With runtime state context
const contextPrompt = buildUserPrompt({
prompt: "show my data",
state: { todos: [{ text: "Buy milk" }] },
});
When currentSpec is provided, the prompt instructs the AI to use the specified edit modes. Available modes:
"patch" — RFC 6902 JSON Patch. One operation per line. Best for precise, targeted single-field updates."merge" — RFC 7396 JSON Merge Patch. Partial object deep-merged; null deletes. Best for structural changes."diff" — Unified diff against the serialized spec. Best for small text-level changes.Format-agnostic utilities for working with specs:
import { deepMergeSpec, diffToPatches } from "@json-render/core";
// RFC 7396 deep merge: null deletes, arrays replace, objects recurse
const merged = deepMergeSpec(baseSpec, { elements: { main: { props: { title: "New" } } } });
// RFC 6902 diff: generate JSON Patch operations from two objects
const patches = diffToPatches(oldSpec, newSpec);
// [{ op: "replace", path: "/elements/main/props/title", value: "New" }]
Validate spec structure and auto-fix common issues:
import { validateSpec, autoFixSpec, formatSpecIssues } from "@json-render/core";
// Validate a spec
const { valid, issues } = validateSpec(spec);
// Format issues for display
console.log(formatSpecIssues(issues));
// Auto-fix common issues (returns a corrected copy)
const fixed = autoFixSpec(spec);
Elements can declare a watch field to trigger actions when state values change. watch is a top-level field on the element (sibling of type, props, children), not inside props.
{
"type": "Select",
"props": {
"label": "Country",
"value": { "$bindState": "/form/country" },
"options": ["US", "Canada", "UK"]
},
"watch": {
"/form/country": {
"action": "loadCities",
"params": { "country": { "$state": "/form/country" } }
}
},
"children": []
}
Watchers only fire on value changes, not on initial render. Multiple action bindings per path execute sequentially.
| Function | Description | Args |
|---|---|---|
required | Value must not be empty | — |
email | Must be a valid email | — |
url | Must be a valid URL | — |
numeric | Must be a number | — |
minLength | Minimum string length | { min: number } |
maxLength | Maximum string length | { max: number } |
min | Minimum numeric value | { min: number } |
max | Maximum numeric value | { max: number } |
pattern | Must match regex | { pattern: string } |
matches | Must equal another field | { other: { $state: "/path" } } |
equalTo | Alias for matches | { other: { $state: "/path" } } |
lessThan | Must be less than another field | { other: { $state: "/path" } } |
greaterThan | Must be greater than another field | { other: { $state: "/path" } } |
requiredIf | Required when condition is truthy | { field: { $state: "/path" } } |
import { check } from "@json-render/core";
check.required("Field is required");
check.email("Invalid email");
check.matches("/form/password", "Passwords must match");
check.equalTo("/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 notifications enabled");
json-render supports completely different spec formats for different renderers:
// React: Flat element map
{ root: "card-1", elements: { "card-1": { type: "Card", props: {...}, children: [...] } } }
// Remotion: Timeline
{ composition: {...}, tracks: [...], clips: [...] }
// Your own: Whatever you need
{ pages: [...], navigation: {...}, theme: {...} }
Each renderer defines its own schema with defineSchema() and its own prompt template.
FAQs
JSON becomes real things. Define your catalog, register your components, let AI generate.
The npm package @json-render/core receives a total of 708,743 weekly downloads. As such, @json-render/core popularity was classified as popular.
We found that @json-render/core demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 3 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
An open letter signed by 50 companies, from NVIDIA and Microsoft to Mistral and Hugging Face, urges Washington not to restrict open weight AI.

Security News
/Research
A fake corepack.org site is impersonating the Node.js tool and delivers an infostealer and proxyware to developers who download it.

Research
/Security News
A large-scale campaign abused GitHub Actions in compromised repositories to exploit CVE-2026-41940 in cPanel and WHM and steal server credentials.