🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@json-render/core

Package Overview
Dependencies
Maintainers
3
Versions
29
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@json-render/core

JSON becomes real things. Define your catalog, register your components, let AI generate.

Source
npmnpm
Version
0.14.0
Version published
Weekly downloads
616K
-9.19%
Maintainers
3
Weekly downloads
 
Created
Source

@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()), // 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
});

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

// 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",
  ],
});

Stream AI Responses (SpecStream)

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.

Low-Level Utilities

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);

API Reference

Schema

ExportPurpose
defineSchema(builder, options?)Create a schema with spec/catalog structure
SchemaBuilderBuilder with s.object(), s.array(), s.map(), etc.

Schema options:

OptionPurpose
promptTemplateCustom AI prompt generator
defaultRulesDefault rules injected before custom rules in prompts
builtInActionsActions always available at runtime, auto-injected into prompts (e.g. setState)

Catalog

ExportPurpose
defineCatalog(schema, data)Create a type-safe catalog from schema
catalog.prompt(options?)Generate AI system prompt

SpecStream

ExportPurpose
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

ExportPurpose
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
ComputedFunctionFunction signature for $computed expressions
PropResolutionContextContext for resolving props (includes functions for $computed)

Validation

ExportPurpose
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
builtInValidationFunctionsAll built-in validation functions
runValidationCheck()Run a single validation check

User Prompt

ExportPurpose
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
UserPromptOptionsOptions type for buildUserPrompt
EditMode"patch" | "merge" | "diff"
EditConfigConfiguration for edit modes ({ modes: EditMode[] })
BuildEditUserPromptOptionsOptions type for buildEditUserPrompt

Merge and Diff

ExportPurpose
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

Spec Validation

ExportPurpose
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

ExportPurpose
ActionBindingAction binding with action, params, confirm, preventDefault, etc.
BuiltInActionBuilt-in action definition with name and description

Inline Mode (Mixed Streams)

ExportPurpose
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_TYPEConstants 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.

State Store

ExportPurpose
createStateStore(initialState?)Create a framework-agnostic in-memory StateStore
StateStoreInterface for plugging in external state management (Redux, Zustand, XState, etc.)
StateModelState 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.

Store Utilities (for adapter authors)

Available via @json-render/core/store-utils:

ExportPurpose
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
StoreAdapterConfigConfig 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.

Types

ExportPurpose
SpecBase spec type
CatalogCatalog type
BuiltInActionBuilt-in action type (name + description)
ComputedFunctionFunction signature for $computed expressions
VisibilityConditionVisibility condition type (used by $cond)
VisibilityContextContext for evaluating visibility and prop expressions
SpecStreamLineSingle patch operation
SpecStreamCompilerStreaming 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).

Template ($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 ($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}`,
};

API

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

Visibility conditions control when elements are shown. VisibilityContext is { stateModel: StateModel, repeatItem?: unknown, repeatIndex?: number }.

Syntax

{ "$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

TypeScript Helpers

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] }

User Prompt Builder

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.

Deep Merge and Diff

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" }]

Spec Validation

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);

State Watchers

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.

Validation

Built-in Validation Functions

FunctionDescriptionArgs
requiredValue must not be empty
emailMust be a valid email
urlMust be a valid URL
numericMust be a number
minLengthMinimum string length{ min: number }
maxLengthMaximum string length{ max: number }
minMinimum numeric value{ min: number }
maxMaximum numeric value{ max: number }
patternMust match regex{ pattern: string }
matchesMust equal another field{ other: { $state: "/path" } }
equalToAlias for matches{ other: { $state: "/path" } }
lessThanMust be less than another field{ other: { $state: "/path" } }
greaterThanMust be greater than another field{ other: { $state: "/path" } }
requiredIfRequired when condition is truthy{ field: { $state: "/path" } }

TypeScript Helpers

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");

Custom Schemas

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.

Keywords

json

FAQs

Package last updated on 13 Mar 2026

Did you know?

Socket

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.

Install

Related posts