开源项目

json-render

用 catalog + Zod schema 给 LLM 划好边界,让模型只输出你预设组件与 action 组成的 JSON,再由渲染器还原成真实界面的 Generative UI 框架,支持流式渐进渲染和 $state/$cond 等动态表达式。亮点是同一份 catalog 可落到 React、Vue、Svelte、Solid、React Native、Remotion 视频、PDF、邮件、终端 Ink 甚至 Three.js 场景,自带上百个 shadcn 组件定义和 devtools 面板,还提供 MCP Apps 集成方便接 Claude、Cursor 等客户端,适合做动态仪表盘、报表和个性化界面。Apache-2.0 协议。

README

json-render

生成式 UI(Generative UI)框架。

从 prompt 生成动态、个性化的 UI,同时不牺牲可靠性。预定义组件与 action,确保输出安全、可预测。

Vercel Labs Product npm version: @json-render/core License: Apache-2.0 npm downloads per month: @json-render/core

# for React
npm install @json-render/core @json-render/react
# for React with pre-built shadcn/ui components
npm install @json-render/shadcn
# or for React Native
npm install @json-render/core @json-render/react-native
# or for video
npm install @json-render/core @json-render/remotion
# or for PDF documents
npm install @json-render/core @json-render/react-pdf
# or for HTML email
npm install @json-render/core @json-render/react-email @react-email/components @react-email/render
# or for Vue
npm install @json-render/core @json-render/vue
# or for Svelte
npm install @json-render/core @json-render/svelte
# or for SolidJS
npm install @json-render/core @json-render/solid
# or for terminal UIs
npm install @json-render/core @json-render/ink ink react
# or for full Next.js apps (routes, layouts, SSR, metadata)
npm install @json-render/core @json-render/react @json-render/next
# or for 3D scenes (and gaussian splatting via the GaussianSplat component)
npm install @json-render/core @json-render/react-three-fiber @react-three/fiber @react-three/drei three

为什么选择 json-render?

json-render 是一个 生成式 UI(Generative UI) 框架:AI 从自然语言 prompt 生成界面,并限定在你定义的组件范围内。你设定护栏,AI 在其中生成:

  • 有护栏约束(Guardrailed) - AI 只能使用你 catalog 中的组件
  • 可预测(Predictable) - JSON 输出每次都符合你的 schema
  • 快速(Fast) - 随着模型响应流式、渐进地渲染
  • 跨平台(Cross-Platform) - 同一份 catalog 可用于 React、Vue、Svelte、Solid(Web)以及 React Native(移动端)
  • 开箱即用(Batteries Included) - 36 个预置 shadcn/ui 组件,拿来即用

快速开始

1. 定义你的 Catalog

import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import { z } from "zod";

const catalog = defineCatalog(schema, {
  components: {
    Card: {
      props: z.object({ title: z.string() }),
      description: "A card container",
    },
    Metric: {
      props: z.object({
        label: z.string(),
        value: z.string(),
        format: z.enum(["currency", "percent", "number"]).nullable(),
      }),
      description: "Display a metric value",
    },
    Button: {
      props: z.object({
        label: z.string(),
        action: z.string(),
      }),
      description: "Clickable button",
    },
  },
  actions: {
    export_report: { description: "Export dashboard to PDF" },
    refresh_data: { description: "Refresh all metrics" },
  },
});

2. 定义你的组件

import { defineRegistry, Renderer } from "@json-render/react";

const { registry } = defineRegistry(catalog, {
  components: {
    Card: ({ props, children }) => (
      <div className="card">
        <h3>{props.title}</h3>
        {children}
      </div>
    ),
    Metric: ({ props }) => (
      <div className="metric">
        <span>{props.label}</span>
        <span>{format(props.value, props.format)}</span>
      </div>
    ),
    Button: ({ props, emit }) => (
      <button onClick={() => emit("press")}>{props.label}</button>
    ),
  },
});

3. 渲染 AI 生成的 spec

function Dashboard({ spec }) {
  return <Renderer spec={spec} registry={registry} />;
}

就这么简单。 AI 生成 JSON,你安全地渲染它。


包(Packages)

包 描述
@json-render/core Schema、catalog、AI prompt、动态 props、SpecStream 工具
@json-render/react React renderer、context、hooks
@json-render/vue Vue 3 renderer、composables、providers
@json-render/svelte 基于 runes 响应式的 Svelte 5 renderer
@json-render/solid 具有细粒度响应式 context 的 SolidJS renderer
@json-render/shadcn 36 个预置 shadcn/ui 组件(Radix UI + Tailwind CSS)
@json-render/shadcn-svelte 36 个预置 shadcn-svelte 组件(Svelte 5 + Tailwind CSS)
@json-render/react-three-fiber 用于 3D 场景的 React Three Fiber renderer(20 个内置组件,含 GaussianSplat)
@json-render/react-native 带标准移动端组件的 React Native renderer
@json-render/next Next.js renderer —— JSON 可变成带路由、布局、SSR 的完整 app
@json-render/tanstack-start TanStack Start renderer —— 带路由、布局、SSR 与 head metadata 的完整 app
@json-render/remotion Remotion 视频 renderer、timeline schema
@json-render/react-pdf 用于从 spec 生成 PDF 文档的 React PDF renderer
@json-render/react-email 用于从 spec 生成 HTML/纯文本邮件的 React Email renderer
@json-render/ink 带内置组件的 Ink 终端 renderer,用于交互式 TUI
@json-render/image 通过 Satori 输出 SVG/PNG(OG 图、社交卡片)的 image renderer
@json-render/directives 预置自定义 directive —— $format、$math、$concat、$count、$truncate、$pluralize、$join、$t(i18n)
@json-render/codegen 从 json-render UI 树生成代码的工具
@json-render/devtools 框架无关的 devtools 核心 —— 面板 UI、事件存储、picker、stream tap
@json-render/devtools-react @json-render/devtools 的 React 适配器(可直接使用 <JsonRenderDevtools />)
@json-render/devtools-vue @json-render/devtools 的 Vue 适配器
@json-render/devtools-svelte @json-render/devtools 的 Svelte 适配器
@json-render/devtools-solid @json-render/devtools 的 SolidJS 适配器
@json-render/redux StateStore 的 Redux / Redux Toolkit 适配器
@json-render/zustand StateStore 的 Zustand 适配器
@json-render/jotai StateStore 的 Jotai 适配器
@json-render/xstate StateStore 的 XState Store(atom)适配器
@json-render/mcp 面向 Claude、ChatGPT、Cursor、VS Code 的 MCP Apps 集成
@json-render/yaml 带流式 parser、编辑模式、AI SDK 转换的 YAML 传输格式

Renderer

React(UI)

import { defineRegistry, Renderer } from "@json-render/react";
import { schema } from "@json-render/react/schema";

// Flat spec format (root key + elements map)
const spec = {
  root: "card-1",
  elements: {
    "card-1": {
      type: "Card",
      props: { title: "Hello" },
      children: ["button-1"],
    },
    "button-1": {
      type: "Button",
      props: { label: "Click me" },
      children: [],
    },
  },
};

// defineRegistry creates a type-safe component registry
const { registry } = defineRegistry(catalog, { components });
<Renderer spec={spec} registry={registry} />;

Vue(UI)

import { h } from "vue";
import { defineRegistry, Renderer } from "@json-render/vue";
import { schema } from "@json-render/vue/schema";

const { registry } = defineRegistry(catalog, {
  components: {
    Card: ({ props, children }) =>
      h("div", { class: "card" }, [h("h3", null, props.title), children]),
    Button: ({ props, emit }) =>
      h("button", { onClick: () => emit("press") }, props.label),
  },
});

// In your Vue component template:
// <Renderer :spec="spec" :registry="registry" />

Svelte(UI)

import { defineRegistry, Renderer } from "@json-render/svelte";
import { schema } from "@json-render/svelte/schema";

const { registry } = defineRegistry(catalog, {
  components: {
    Card: ({ props, children }) => /* Svelte 5 snippet */,
    Button: ({ props, emit }) => /* Svelte 5 snippet */,
  },
});

// In your Svelte component:
// <Renderer spec={spec} registry={registry} />

Solid(UI)

import { defineRegistry, Renderer } from "@json-render/solid";
import { schema } from "@json-render/solid/schema";

const { registry } = defineRegistry(catalog, {
  components: {
    Card: (renderProps) => <div>{renderProps.children}</div>,
    Button: (renderProps) => (
      <button onClick={() => renderProps.emit("press")}>
        {renderProps.element.props.label as string}
      </button>
    ),
  },
});

<Renderer spec={spec} registry={registry} />;

shadcn/ui(Web)

import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import { defineRegistry, Renderer } from "@json-render/react";
import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog";
import { shadcnComponents } from "@json-render/shadcn";

// Pick components from the 36 standard definitions
const catalog = defineCatalog(schema, {
  components: {
    Card: shadcnComponentDefinitions.Card,
    Stack: shadcnComponentDefinitions.Stack,
    Heading: shadcnComponentDefinitions.Heading,
    Button: shadcnComponentDefinitions.Button,
  },
  actions: {},
});

// Use matching implementations
const { registry } = defineRegistry(catalog, {
  components: {
    Card: shadcnComponents.Card,
    Stack: shadcnComponents.Stack,
    Heading: shadcnComponents.Heading,
    Button: shadcnComponents.Button,
  },
});

<Renderer spec={spec} registry={registry} />;

React Native(移动端)

import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react-native/schema";
import {
  standardComponentDefinitions,
  standardActionDefinitions,
} from "@json-render/react-native/catalog";
import { defineRegistry, Renderer } from "@json-render/react-native";

// 25+ standard components included
const catalog = defineCatalog(schema, {
  components: { ...standardComponentDefinitions },
  actions: standardActionDefinitions,
});

const { registry } = defineRegistry(catalog, { components: {} });
<Renderer spec={spec} registry={registry} />;

Remotion(视频)

import { Player } from "@remotion/player";
import {
  Renderer,
  schema,
  standardComponentDefinitions,
} from "@json-render/remotion";

// Timeline spec format
const spec = {
  composition: {
    id: "video",
    fps: 30,
    width: 1920,
    height: 1080,
    durationInFrames: 300,
  },
  tracks: [{ id: "main", name: "Main", type: "video", enabled: true }],
  clips: [
    {
      id: "clip-1",
      trackId: "main",
      component: "TitleCard",
      props: { title: "Hello" },
      from: 0,
      durationInFrames: 90,
    },
  ],
  audio: { tracks: [] },
};

<Player
  component={Renderer}
  inputProps={{ spec }}
  durationInFrames={spec.composition.durationInFrames}
  fps={spec.composition.fps}
  compositionWidth={spec.composition.width}
  compositionHeight={spec.composition.height}
/>;

React PDF(文档)

import { renderToBuffer } from "@json-render/react-pdf";

const spec = {
  root: "doc",
  elements: {
    doc: {
      type: "Document",
      props: { title: "Invoice" },
      children: ["page-1"],
    },
    "page-1": {
      type: "Page",
      props: { size: "A4" },
      children: ["heading-1", "table-1"],
    },
    "heading-1": {
      type: "Heading",
      props: { text: "Invoice #1234", level: "h1" },
      children: [],
    },
    "table-1": {
      type: "Table",
      props: {
        columns: [
          { header: "Item", width: "60%" },
          { header: "Price", width: "40%", align: "right" },
        ],
        rows: [
          ["Widget A", "$10.00"],
          ["Widget B", "$25.00"],
        ],
      },
      children: [],
    },
  },
};

// Render to buffer, stream, or file
const buffer = await renderToBuffer(spec);

React Email(邮件)

import { renderToHtml } from "@json-render/react-email";
import { schema, standardComponentDefinitions } from "@json-render/react-email";
import { defineCatalog } from "@json-render/core";

const catalog = defineCatalog(schema, {
  components: standardComponentDefinitions,
});

const spec = {
  root: "html-1",
  elements: {
    "html-1": {
      type: "Html",
      props: { lang: "en", dir: "ltr" },
      children: ["head-1", "body-1"],
    },
    "head-1": { type: "Head", props: {}, children: [] },
    "body-1": {
      type: "Body",
      props: { style: { backgroundColor: "#f6f9fc" } },
      children: ["container-1"],
    },
    "container-1": {
      type: "Container",
      props: {
        style: { maxWidth: "600px", margin: "0 auto", padding: "20px" },
      },
      children: ["heading-1", "text-1"],
    },
    "heading-1": { type: "Heading", props: { text: "Welcome" }, children: [] },
    "text-1": {
      type: "Text",
      props: { text: "Thanks for signing up." },
      children: [],
    },
  },
};

const html = await renderToHtml(spec);

Image(SVG/PNG)

import { renderToPng } from "@json-render/image/render";

const spec = {
  root: "frame",
  elements: {
    frame: {
      type: "Frame",
      props: { width: 1200, height: 630, backgroundColor: "#1a1a2e" },
      children: ["heading"],
    },
    heading: {
      type: "Heading",
      props: { text: "Hello World", level: "h1", color: "#ffffff" },
      children: [],
    },
  },
};

// Render to PNG (requires @resvg/resvg-js)
const png = await renderToPng(spec, { fonts });

// Or render to SVG string
import { renderToSvg } from "@json-render/image/render";
const svg = await renderToSvg(spec, { fonts });

Three.js(3D)

import { defineCatalog } from "@json-render/core";
import { schema, defineRegistry } from "@json-render/react";
import {
  threeComponentDefinitions,
  threeComponents,
  ThreeCanvas,
} from "@json-render/react-three-fiber";

const catalog = defineCatalog(schema, {
  components: {
    Box: threeComponentDefinitions.Box,
    Sphere: threeComponentDefinitions.Sphere,
    AmbientLight: threeComponentDefinitions.AmbientLight,
    DirectionalLight: threeComponentDefinitions.DirectionalLight,
    GaussianSplat: threeComponentDefinitions.GaussianSplat,
    OrbitControls: threeComponentDefinitions.OrbitControls,
  },
  actions: {},
});

const { registry } = defineRegistry(catalog, {
  components: {
    Box: threeComponents.Box,
    Sphere: threeComponents.Sphere,
    AmbientLight: threeComponents.AmbientLight,
    DirectionalLight: threeComponents.DirectionalLight,
    GaussianSplat: threeComponents.GaussianSplat,
    OrbitControls: threeComponents.OrbitControls,
  },
});

<ThreeCanvas
  spec={spec}
  registry={registry}
  shadows
  camera={{ position: [5, 5, 5], fov: 50 }}
  style={{ width: "100%", height: "100vh" }}
/>;

Next.js(完整 App)

import type { NextAppSpec } from "@json-render/next";
import { createNextApp } from "@json-render/next/server";
import { NextAppProvider } from "@json-render/next";

const spec: NextAppSpec = {
  metadata: { title: { default: "My App", template: "%s | My App" } },
  layouts: {
    main: {
      root: "shell",
      elements: {
        shell: { type: "Container", props: {}, children: ["nav", "slot"] },
        nav: { type: "NavBar", props: {}, children: [] },
        slot: { type: "Slot", props: {}, children: [] },
      },
    },
  },
  routes: {
    "/": {
      layout: "main",
      metadata: { title: "Home" },
      page: {
        root: "hero",
        elements: {
          hero: { type: "Card", props: { title: "Welcome" }, children: [] },
        },
      },
    },
  },
};

// Server: creates Page, generateMetadata, generateStaticParams
const app = createNextApp({ spec });

// Client: wrap your layout with NextAppProvider
// <NextAppProvider registry={registry} handlers={handlers}>
//   {children}
// </NextAppProvider>

TanStack Start(完整 App)

import { createFileRoute, notFound } from "@tanstack/react-router";
import {
  PageRenderer,
  StartErrorBoundary,
  StartLoading,
  StartNotFound,
  type StartAppSpec,
} from "@json-render/tanstack-start";
import { createStartApp } from "@json-render/tanstack-start/server";

const spec: StartAppSpec = {
  metadata: { title: { default: "My App", template: "%s | My App" } },
  routes: {
    "/": {
      metadata: { title: "Home" },
      page: {
        root: "hero",
        elements: {
          hero: { type: "Card", props: { title: "Welcome" }, children: [] },
        },
      },
    },
  },
};

const { getPageData, getHead } = createStartApp({ spec });

export const Route = createFileRoute("/$")({
  loader: async ({ location }) => {
    const data = await getPageData({ pathname: location.pathname });
    if (!data) throw notFound();
    return data;
  },
  head: ({ match }) => getHead({ pathname: match.pathname }),
  component: () => <PageRenderer {...Route.useLoaderData()} />,
  pendingComponent: StartLoading,
  errorComponent: StartErrorBoundary,
  notFoundComponent: StartNotFound,
});

用 <StartAppProvider spec={spec}> 包裹根路由的 outlet,这样路由的 fallback 组件就能解析出当前路由。通过它的 functions prop 传入具名的 $computed 实现。

shadcn-svelte(Svelte)

import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/svelte/schema";
import { defineRegistry, Renderer } from "@json-render/svelte";
import { shadcnComponentDefinitions } from "@json-render/shadcn-svelte/catalog";
import { shadcnComponents } from "@json-render/shadcn-svelte";

const catalog = defineCatalog(schema, {
  components: {
    Card: shadcnComponentDefinitions.Card,
    Stack: shadcnComponentDefinitions.Stack,
    Heading: shadcnComponentDefinitions.Heading,
    Button: shadcnComponentDefinitions.Button,
  },
  actions: {},
});

const { registry } = defineRegistry(catalog, {
  components: {
    Card: shadcnComponents.Card,
    Stack: shadcnComponents.Stack,
    Heading: shadcnComponents.Heading,
    Button: shadcnComponents.Button,
  },
});

// In your Svelte component:
// <Renderer spec={spec} registry={registry} />

Devtools

面向任何 json-render app 的即插即用检查面板。包含 spec 树、状态编辑器、action 日志、stream 日志、catalog 浏览器、DOM picker。

// React
import { JsonRenderDevtools } from "@json-render/devtools-react";

<JSONUIProvider registry={registry} handlers={handlers}>
  <Renderer spec={spec} registry={registry} />
  <JsonRenderDevtools spec={spec} catalog={catalog} messages={messages} />
</JSONUIProvider>;

右下角会出现一个浮动开关。快捷键:Ctrl/Cmd + Shift + J。生产环境会被 tree-shake 成 null。

可用于 React、Vue、Svelte 和 Solid —— 只需把 @json-render/devtools-react 换成与你 renderer 匹配的适配器。

Ink(终端)

import { defineCatalog } from "@json-render/core";
import {
  schema,
  standardComponentDefinitions,
  standardActionDefinitions,
  defineRegistry,
  Renderer,
  JSONUIProvider,
} from "@json-render/ink";

const catalog = defineCatalog(schema, {
  components: { ...standardComponentDefinitions },
  actions: standardActionDefinitions,
});

const { registry } = defineRegistry(catalog, { components: {} });

const spec = {
  root: "card-1",
  elements: {
    "card-1": {
      type: "Card",
      props: { title: "Status" },
      children: ["status-1"],
    },
    "status-1": {
      type: "StatusLine",
      props: { label: "Build", status: "success" },
      children: [],
    },
  },
};

<JSONUIProvider initialState={{}}>
  <Renderer spec={spec} registry={registry} />
</JSONUIProvider>;

特性

流式处理(SpecStream)

流式、渐进地处理 AI 响应:

import { createSpecStreamCompiler } from "@json-render/core";

const compiler = createSpecStreamCompiler<MySpec>();

// Process chunks as they arrive
const { result, newPatches } = compiler.push(chunk);
setSpec(result); // Update UI with partial result

// Get final result
const finalSpec = compiler.getResult();

AI Prompt 生成

从你的 catalog 生成 system prompt:

const systemPrompt = catalog.prompt();
// Includes component descriptions, props schemas, available actions

条件可见性

{
  "type": "Alert",
  "props": { "message": "Error occurred" },
  "visible": [
    { "$state": "/form/hasError" },
    { "$state": "/form/errorDismissed", "not": true }
  ]
}

动态 Props

任何 prop 值都可以通过表达式由数据驱动:

{
  "type": "Icon",
  "props": {
    "name": {
      "$cond": { "$state": "/activeTab", "eq": "home" },
      "$then": "home",
      "$else": "home-outline"
    },
    "color": {
      "$cond": { "$state": "/activeTab", "eq": "home" },
      "$then": "#007AFF",
      "$else": "#8E8E93"
    }
  }
}

表达式形式:

  • { "$state": "/state/key" } - 从 state model 中读取一个值
  • { "$cond": <condition>, "$then": <value>, "$else": <value> } - 评估条件并选择一个分支
  • { "$template": "Hello, ${/user/name}!" } - 将 state 值插值到字符串中
  • { "$computed": "fn", "args": { ... } } - 用已解析的 args 调用注册的函数

Actions

组件可以触发 action,包括内置的 setState action:

{
  "type": "Pressable",
  "props": {
    "action": "setState",
    "actionParams": { "statePath": "/activeTab", "value": "home" }
  },
  "children": ["home-icon"]
}

setState action 直接更新 state model,进而重新评估可见性条件和动态 prop 表达式。

State Watchers

通过触发 action 来响应 state 变化:

{
  "type": "Select",
  "props": {
    "value": { "$bindState": "/form/country" },
    "options": ["US", "Canada", "UK"]
  },
  "watch": {
    "/form/country": {
      "action": "loadCities",
      "params": { "country": { "$state": "/form/country" } }
    }
  }
}

watch 是元素上的顶层字段(与 type/props/children 同级)。Watcher 在被监听的值发生变化时触发,而不是在初始渲染时触发。


演示

git clone https://github.com/vercel-labs/json-render
cd json-render
pnpm install
pnpm dev
  • http://json-render.localhost:1355 - 文档与 Playground
  • http://dashboard-demo.json-render.localhost:1355 - 示例 Dashboard
  • http://react-email-demo.json-render.localhost:1355 - React Email 示例
  • http://remotion-demo.json-render.localhost:1355 - Remotion 视频示例
  • Chat 示例:在 examples/chat 中运行 pnpm dev
  • 实验性 Jev 组合:使用 core 中的 experimental_composeSpec 和 experimental_createEvaluator 搭配你自己的 catalog,或在 /playground 中选择 Jev (Experimental)。尚未发布;源码构建说明见该指南。
  • Svelte 示例:在 examples/svelte 或 examples/svelte-chat 中运行 pnpm dev
  • Vue 示例:在 examples/vue 中运行 pnpm dev
  • Vite Renderers(React + Vue + Svelte + Solid):在 examples/vite-renderers 中运行 pnpm dev
  • React Native 示例:在 examples/react-native 中运行 npx expo start
  • Gaussian Splatting(R3F):在 examples/react-three-fiber-gsplat 中运行 pnpm dev
  • Gaussian Splatting(实验性独立 gsplat.js 示例):在 examples/gsplat 中运行 pnpm dev

工作原理

flowchart LR
    A[User Prompt] --> B[AI + Catalog]
    B --> C[JSON Spec]
    C --> D[Renderer]

    B -.- E([guardrailed])
    C -.- F([predictable])
    D -.- G([streamed])
  1. 定义护栏 - 规定 AI 可以使用哪些组件、action 和数据绑定
  2. 写 Prompt - 用自然语言描述你想要的东西
  3. AI 生成 JSON - 输出始终可预测,限定在你的 catalog 内
  4. 快速渲染 - 随着模型响应流式、渐进地渲染

License

Apache-2.0

开源项目vercel-labs2026-09-20原文

相关内容