json-render

Framework

vercel-labs/json-render

Generative UI framework rendering AI-generated JSON into safe, predictable UIs.

Overview

json-render is a Generative UI framework where AI builds interfaces from prompts, constrained to components you define. JSON output matches your schema for safe, predictable streaming rendering. Renderers cover React, Vue, Svelte, React Native, 3D, video, PDF, email, and terminal UIs.

README Preview

# json-render\n\n**The Generative UI framework.**\n\nGenerate dynamic, personalized UIs from prompts without sacrificing reliability. Predefined components and actions for safe, predictable output.\n\n\n  \n  \n  \n  \n\n\n```bash\n# for React\nnpm install @json-render/core @json-render/react\n# for React with pre-built shadcn/ui components\nnpm install @json-render/shadcn\n# or for React Native\nnpm install @json-render/core @json-render/react-native\n# or for video\nnpm install @json-render/core @json-render/remotion\n# or for PDF documents\nnpm install @json-render/core @json-render/react-pdf\n# or for HTML email\nnpm install @json-render/core @json-render/react-email @react-email/components @react-email/render\n# or for Vue\nnpm install @json-render/core @json-render/vue\n# or for Svelte\nnpm install @json-render/core @json-render/svelte\n# or for SolidJS\nnpm install @json-render/core @json-render/solid\n# or for terminal UIs\nnpm install @json-render/core @json-render/ink ink react\n# or for full Next.js apps (routes, layouts, SSR, metadata)\nnpm install @json-render/core @json-render/react @json-render/next\n# or for 3D scenes (and gaussian splatting via the GaussianSplat component)\nnpm install @json-render/core @json-render/react-three-fiber @react-three/fiber @react-three/drei three\n```\n\n## Why json-render?\n\njson-render is a **Generative UI** framework: AI generates interfaces from natural language prompts, constrained to components you define. You set the guardrails, AI generates within them:\n\n- **Guardrailed** - AI can only use components in your catalog\n- **Predictable** - JSON output matches your schema, every time\n- **Fast** - Stream and render progressively as the model responds\n- **Cross-Platform** - React, Vue, Svelte, Solid (web), React Native (mobile) from the same catalog\n- **Batteries Included** - 36 pre-built shadcn/ui components ready to use\n\n## Quick Start\n\n### 1. Define Your Catalog\n\n```typescript\nimport { defineCatalog } 

FAQ (5)

Troubleshooting
How to fix "Invalid input: expected nonoptional, received undefined" visible field error in @json-render after upgrading to Zod 4.4?

This is a Zod 4.4 compatibility bug in @json-render (≤0.19.0), where the optional visible field on elements is wrongly treated as required during catalog.validate(). Fix: upgrade all @json-render packages (core, react, shadcn, react-email) to v0.20.0 or later — the fix landed in PR #299 and is verified working on v0.21.1 with zod 4.6. If you can't upgrade immediately, pin zod to 4.3.6 as a temporary workaround (pnpm add zod@4.3.6), since any zod 4.4.x version triggers the failure.

GitHub Issue #287
Troubleshooting
How to fix 'value is not a function' error when using useBoundProp in @json-render/solid?

The README example was incorrect: useBoundProp(propsValue, binding) returns a tuple [T | undefined, (value: T) => void] where the first element is the raw prop value, not a Solid accessor. Do not call it as value(). Correct usage:

Input: (ctx) => {
  const [value, setValue] = useBoundProp(ctx.props.value, ctx.bindings?.value);
  return (
    <input
      value={String(value ?? "")}
      onInput={(event) => setValue(event.currentTarget.value)}
    />
  );
}

Read the resolved value directly inside JSX so Solid tracks reactive reads, and use the setter (setValue) for updates. The documentation example was corrected in PR #401 (together with #381), covering initial values, external state updates, and null values.

GitHub Issue #382
Troubleshooting
Why does 'Cannot read properties of undefined (reading props)' occur in @json-render/solid defineRegistry callbacks?

The defineRegistry() callback context does not contain an element field — older README examples incorrectly used renderProps.element.props. The callback receives resolved props directly via ctx.props (along with children, emit, on, bindings, and loading). Fix your component callbacks to read props from the context root:

const { registry } = defineRegistry(catalog, {
  components: {
    Card: (ctx) => (
      <div class="card">
        <h3>{ctx.props.title as string}</h3>
        {ctx.children}
      </div>
    ),
  },
});

Note: only the lower-level ComponentRenderer contract exposes element; the defineRegistry() adapter does not. The README examples were corrected in PR #401 to use the resolved context, keep input values reactive, and include the renderer's required providers.

GitHub Issue #381
Troubleshooting
How to fix 'setValue does not exist on useFieldValidation' error in json-render?

useFieldValidation only handles validation (returns errors and validate) — it does not provide value or setValue. Get those from the useBoundProp hook instead. Correct usage:

import { useBoundProp, useFieldValidation } from '@json-render/react';

function TextField({ element }) {
  const [value, setValue] = useBoundProp(element.props.valuePath);
  const { errors, validate } = useFieldValidation(
    element.props.valuePath,
    element.props.checks
  );

  return (
    <div>
      <label>{element.props.label}</label>
      <input
        value={value || ''}
        onChange={(e) => setValue(e.target.value)}
        onBlur={() => validate()}
      />
      {errors.map((error, i) => (
        <p key={i} className="text-red-500 text-sm">{error}</p>
      ))}
    </div>
  );
}

This was a documentation error fixed in issue #107. Setting data directly without setValue from useBoundProp bypasses the binding layer, which is why validation did not trigger correctly.

GitHub Issue #47
Troubleshooting
Why does $item return a path string like "list/0/url" instead of the actual value in json-render action params?

In json-render 0.19.0 (React), using { "$item": "url" } inside action.params within a repeated element (repeat on /list) incorrectly resolves to the JSON path string (e.g., "list/0/url") instead of the field's actual value. **Workaround:** Replace $item with $bindItem in action params: "params": { "url": { "$bindItem": "url" } } — this correctly returns the actual field value.

GitHub Issue #292