学习有两忌,自高和自狭。——书摘

json-render:让 AI 用 JSON 生成界面,也让界面始终可控

项目地址:https://github.com/vercel-labs/json-render

当 AI 开始参与产品界面生成,最令人期待的画面往往很美好。

用户说出一句自然语言需求,系统便迅速生成一块仪表盘、一张数据卡片、一份 PDF、一封邮件,甚至是一段视频或一个完整应用页面。界面不再完全依赖手工拼装,内容、布局与交互仿佛都可以随着需求自然生长。

但另一面的问题同样清晰。

如果 AI 可以任意输出界面,组件从哪里来,样式如何保持一致,交互如何约束,数据如何绑定,渲染结果如何保证稳定,最终生成的内容又如何真正落入应用程序之中。

json-render 给出的方向,是把 AI 的生成能力放进一套明确的边界里。

它是一个 Generative UI 框架。AI 可以根据自然语言提示生成动态、个性化的界面,而开发者则提前定义好可用组件、动作与数据绑定方式。AI 不直接生成不可预测的前端代码,而是生成符合既定 Schema 的 JSON Spec,再由渲染器把这些 JSON 描述转化为真正的界面。

这让生成式 UI 不再像一场完全开放的即兴表演,而更像在一套清晰舞台规则中的创作。

AI 负责生成,开发者负责划定舞台

json-render 的核心逻辑非常直接。

开发者先定义组件目录。

目录中写明有哪些组件可以被使用,每个组件有哪些属性,属性应当满足什么结构,组件又承担什么职责。动作同样可以被定义,例如导出报告、刷新数据、更新状态。

随后,AI 根据组件目录生成 JSON。

最终,渲染器读取 JSON Spec,并将其中的元素映射到真实的 React、Vue、Svelte、Solid 或其他平台组件。

整个过程可以概括为:

1
2
3
4
5
6
7
8
9
用户提示

AI 与组件目录

JSON Spec

渲染器

真实界面

在这条链路中,组件目录是规则,JSON 是生成结果,渲染器则是把描述变成界面的执行者。

AI 并不拥有无限制的组件调用权。它只能使用目录中已经定义的组件,也只能生成满足 Schema 的属性结构。开发者提前搭好边界,AI 在边界内组织界面。

这种设计让生成式 UI 同时具备两个看似相反的特点。

它可以动态生成。

它也可以保持可预测。

从自然语言到 JSON,而不是从自然语言到不可控代码

传统的前端开发中,界面通常由开发者直接编写组件代码、样式与交互逻辑。

在 json-render 中,AI 生成的不是任意 JSX,不是任意 CSS,也不是一段需要直接执行的前端代码。它生成的是 JSON Spec。

这份 Spec 描述了界面的结构。

它可以说明哪个元素是根节点,哪些组件存在,它们各自拥有怎样的属性,以及它们之间的父子关系。

例如,一个卡片中可以放入一个按钮:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"root": "card-1",
"elements": {
"card-1": {
"type": "Card",
"props": {
"title": "Hello"
},
"children": ["button-1"]
},
"button-1": {
"type": "Button",
"props": {
"label": "Click me"
},
"children": []
}
}
}

这里没有让 AI 自由写出一套新的卡片实现,也没有要求 AI 自己处理 DOM、样式系统或组件生命周期。

AI 只需要在允许的组件类型中进行组合。

而真正的 CardButton 如何呈现,仍由开发者提供的组件实现决定。

这让界面生成的创造性与应用实现的确定性分开了。

AI 可以决定如何组合。

应用依旧决定组件是什么。

从一个组件目录开始

json-render 的 Quick Start 从定义 Catalog 开始。

Catalog 是 AI 可以使用的界面语言。开发者在这里声明组件、属性 Schema、组件描述,以及允许触发的动作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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",
},
},
});

这份定义像是一张界面能力清单。

卡片可以拥有标题。

指标组件可以展示标签、数值与格式。

按钮可以携带标签与动作。

系统可以导出报告,也可以刷新数据。

AI 接收到的并不是一个空白画布,而是一套经过开发者设计的可用元素。它知道可以使用什么,也知道每种元素应当带着哪些信息。

这正是 json-render 所说的 Guardrailed。

生成不是无限制的。

生成是在组件目录规定的护栏之内进行的。

同一份目录,连接 AI 描述与真实组件

定义完 Catalog 后,开发者需要注册真实组件实现。

在 React 中,可以使用 defineRegistry 建立组件注册表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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>
),
},
});

Catalog 定义的是 AI 可以描述什么。

Registry 定义的则是应用实际如何渲染这些描述。

两者形成了一种明确对应关系。

AI 生成 Card

Registry 提供 Card 的真实实现。

AI 生成 Metric

Registry 提供 Metric 的真实实现。

AI 生成 Button

Registry 提供按钮的展示方式与事件处理。

最终,渲染过程可以非常简洁:

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

AI 生成 JSON,应用安全地渲染 JSON。

这就是 json-render 最核心的工作方式。

界面可以流式出现,而不是等待全部生成完成

生成式体验的一个关键感受来自速度。

如果用户必须等待 AI 完整输出一份巨大 JSON,才能看到第一个组件,界面生成的过程就会显得迟缓。json-render 提供了 SpecStream,用于随着模型响应逐步处理和渲染 Spec。

1
2
3
4
5
6
7
8
9
import { createSpecStreamCompiler } from "@json-render/core";

const compiler = createSpecStreamCompiler<MySpec>();

const { result, newPatches } = compiler.push(chunk);

setSpec(result);

const finalSpec = compiler.getResult();

当模型持续返回内容时,SpecStream 可以处理到达的分块,得到逐步更新的结果,并将当前界面状态交给渲染层。

这让界面不必等到最后一刻才完整出现。

它可以随着模型的输出逐渐生长。

一张卡片可以先出现。

随后,指标、按钮、图表或其他元素继续抵达。

用户看到的不是一个长期空白的等待状态,而是一份正在被构建的界面。

这种流式能力也与 json-render 的生成路径保持一致:AI 负责输出 Spec,SpecStream 负责接住过程,Renderer 负责把过程中的结果持续呈现出来。

Catalog 还能变成 AI 的系统提示词

AI 要想生成符合规则的 JSON,首先需要理解规则。

json-render 可以从 Catalog 自动生成系统提示词:

1
const systemPrompt = catalog.prompt();

生成的提示词会包含组件描述、属性 Schema 与可用动作。

这让 Catalog 不只是运行时的一份类型与验证定义,也成为 AI 理解界面能力的语言来源。

开发者在组件目录中写下的内容,同时服务于多个环节:

  • 约束 AI 可使用的组件
  • 描述组件属性结构
  • 提供组件语义说明
  • 定义可调用动作
  • 生成 AI 所需的系统提示词
  • 为渲染器建立组件映射依据

一份组件目录,连接了模型、Schema、组件实现与渲染过程。

这使得 Generative UI 的规则不必散落在多处,而可以围绕 Catalog 被集中组织起来。

支持多种前端框架与输出形态

json-render 并不只面向某一种 Web 框架。

它提供 React、Vue、Svelte 与 Solid 的渲染器支持,让同一套组件目录能够面向不同 Web 技术栈工作。

它也支持 React Native,用于移动端界面。

它支持 Next.js 与 TanStack Start,可以让 JSON Spec 进入路由、布局、服务端渲染与元数据等完整应用结构。

它还支持视频、PDF、电子邮件、图片、终端界面与 3D 场景。

这意味着,JSON Spec 不只可以变成网页中的组件树。

它还可以变成:

  • React 界面
  • Vue 界面
  • Svelte 界面
  • Solid 界面
  • React Native 移动端界面
  • Next.js 应用页面
  • TanStack Start 应用页面
  • Remotion 视频时间线
  • PDF 文档
  • HTML 或纯文本邮件
  • SVG 或 PNG 图像
  • Ink 终端界面
  • React Three Fiber 3D 场景

同一个生成式 UI 思路,可以随着渲染器延伸到不同的输出媒介中。

AI 不只是生成一块网页面板。

它可以生成一份可渲染的结构化描述,而不同平台的 Renderer 决定这份描述最终以怎样的形态出现。

为 Web 准备的组件,也可以来自 shadcn/ui

对于希望快速获得一组现成 Web 组件的场景,json-render 提供了 @json-render/shadcn

该包包含 36 个预构建的 shadcn/ui 组件,基于 Radix UI 与 Tailwind CSS。

开发者可以从标准定义中选择需要的组件,再将对应实现注册到 Registry 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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";

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

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

这让开发者可以从已有组件能力出发,为 AI 准备一个受控的界面词汇表。

AI 看到的是 CardStackHeadingButton 等可组合元素。

应用得到的则是与这些定义匹配的真实组件实现。

动态属性,让界面不是静态结构

生成的界面并不一定只能展示固定内容。

json-render 支持 Dynamic Props,也就是通过表达式让属性值与状态模型关联。

例如,一个图标可以根据当前激活标签页变换名称与颜色:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"type": "Icon",
"props": {
"name": {
"$cond": {
"$state": "/activeTab",
"eq": "home"
},
"$then": "home",
"$else": "home-outline"
},
"color": {
"$cond": {
"$state": "/activeTab",
"eq": "home"
},
"$then": "#007AFF",
"$else": "#8E8E93"
}
}
}

json-render 提供了多种表达形式。

读取状态:

1
2
3
{
"$state": "/state/key"
}

根据条件选择分支:

1
2
3
4
5
6
7
8
{
"$cond": {
"$state": "/activeTab",
"eq": "home"
},
"$then": "home",
"$else": "home-outline"
}

在字符串中插入状态值:

1
2
3
{
"$template": "Hello, ${/user/name}!"
}

调用已注册函数:

1
2
3
4
{
"$computed": "fn",
"args": {}
}

这些表达式让 JSON Spec 不再只是一次性生成的静态描述。

它可以读取当前状态,随着状态变化呈现不同内容,并在既定规则内拥有更丰富的动态表现。

条件可见性,让组件知道何时出现

界面中的组件并不总应该始终可见。

例如,一个错误提示应当只在表单存在错误、且用户尚未关闭提示时出现。json-render 可以通过 visible 定义条件可见性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"type": "Alert",
"props": {
"message": "Error occurred"
},
"visible": [
{
"$state": "/form/hasError"
},
{
"$state": "/form/errorDismissed",
"not": true
}
]
}

条件可见性使得 AI 生成的界面不仅能组织静态布局,还能在状态变化时决定哪些元素应当展示。

当状态模型中的值发生变化,相关条件可以重新被评估,界面也随之更新。

这让 Spec 可以表达更接近真实应用的界面逻辑。

有些内容在满足条件时出现。

有些内容在用户操作后消失。

有些组件则随着数据状态而改变自己的存在方式。

动作让组件从展示走向交互

生成式 UI 如果只能展示信息,仍然只是一个动态页面。

json-render 同时允许组件触发动作。

例如,组件可以使用内置的 setState 动作直接更新状态模型:

1
2
3
4
5
6
7
8
9
10
11
{
"type": "Pressable",
"props": {
"action": "setState",
"actionParams": {
"statePath": "/activeTab",
"value": "home"
}
},
"children": ["home-icon"]
}

setState 更新状态后,条件可见性与动态属性表达式都会重新计算。

按钮被点击。

状态发生变化。

界面根据状态重新呈现。

这条链路使 JSON Spec 具备了交互能力。

AI 可以在可用动作范围内组织交互入口,应用则继续掌握动作真正如何被处理。生成式界面不只是把内容摆出来,还可以通过状态与动作形成可操作的使用体验。

State Watchers,让状态变化触发动作

除了由用户主动点击触发动作,json-render 还支持 State Watchers。

Watcher 可以监听某个状态路径,并在值发生变化时触发相应动作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"type": "Select",
"props": {
"value": {
"$bindState": "/form/country"
},
"options": ["US", "Canada", "UK"]
},
"watch": {
"/form/country": {
"action": "loadCities",
"params": {
"country": {
"$state": "/form/country"
}
}
}
}
}

在这个例子中,选择组件绑定到 /form/country

当国家值发生变化时,loadCities 动作会被触发,并读取当前国家值作为参数。

Watcher 位于元素顶层,与 typepropschildren 同级。它只在监听值发生变化时触发,而不会在首次渲染时自动运行。

这让 JSON Spec 可以表达更多由状态变化驱动的界面行为。

选择国家后加载城市。

切换选项后刷新内容。

修改条件后更新相关数据。

界面不再只是响应一次点击,而可以围绕状态流动形成更连续的交互过程。

从仪表盘到视频,从 PDF 到邮件

json-render 的渲染器能力,使生成式界面可以进入许多不同场景。

在 Remotion 中,Spec 可以描述视频合成信息、轨道与剪辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import { Player } from "@remotion/player";
import {
Renderer,
schema,
standardComponentDefinitions,
} from "@json-render/remotion";

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}
/>;

在 PDF 场景中,Spec 可以描述文档、页面、标题与表格:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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: [],
},
},
};

const buffer = await renderToBuffer(spec);

在邮件场景中,Spec 可以描述 HTML、Head、Body、容器、标题与文本,并最终被渲染为 HTML:

1
2
3
import { renderToHtml } from "@json-render/react-email";

const html = await renderToHtml(spec);

在图像场景中,Spec 可以被渲染为 PNG 或 SVG:

1
2
3
4
5
import { renderToPng } from "@json-render/image/render";

const png = await renderToPng(spec, {
fonts,
});

在 3D 场景中,json-render 支持 React Three Fiber,并提供包括 GaussianSplat 在内的组件能力。

在终端中,json-render 可以通过 Ink 渲染交互式 TUI。

一份结构化 Spec,可以根据不同 Renderer 去往不同地方。

它可以成为页面。

可以成为应用。

可以成为文档。

可以成为邮件。

可以成为图像。

可以成为视频。

也可以成为终端中的界面。

Next.js 与 TanStack Start:让 JSON 进入完整应用结构

json-render 并不只用于渲染单一组件树。

通过 @json-render/next,JSON 可以进入 Next.js 应用的路由、布局、服务端渲染与元数据结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import type { NextAppSpec } from "@json-render/next";
import { createNextApp } from "@json-render/next/server";

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: [],
},
},
},
},
},
};

const app = createNextApp({
spec,
});

TanStack Start 也拥有对应的完整应用渲染能力,可以基于 Spec 处理路由、页面数据、头部信息、加载状态、错误边界与未找到页面等内容。

这让 json-render 的目标不只停留在“AI 生成一块 UI”。

它也可以让结构化描述进入应用层级,参与组织页面、布局与路由。

Devtools,让生成过程与界面树可被观察

当应用开始动态生成 UI,开发者需要能够看见生成结果、状态变化与动作流动。

json-render 提供了 Devtools。

它可以作为检查面板接入应用,提供 Spec Tree、状态编辑器、动作日志、流日志、Catalog 浏览器与 DOM Picker。

在 React 中,可以这样接入:

1
2
3
4
5
6
7
8
9
10
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>;

开发环境中,浮动开关会出现在右下角,也可以通过快捷键打开。

Devtools 支持 React、Vue、Svelte 与 Solid,对应替换不同的适配包即可。

对于生成式 UI 而言,开发工具的重要性并不只是检查样式。

开发者还需要观察 AI 生成了怎样的 Spec,Spec 如何变化,状态如何更新,动作何时触发,流式输出如何抵达。Devtools 为这些过程提供了一个可以被查看与理解的窗口。

一个聊天界面,也可以直接流入丰富交互 UI

项目中提供了一个 Chat Example。

这个示例展示了一个由 AI 驱动的数据探索器,它能够在聊天界面中直接流式呈现丰富的交互式 UI。

服务端通过 pipeJsonRender 合并 AI SDK 的 UI 流与 json-render Spec Patch,使文本内容、工具调用指示与渲染后的界面可以在同一条聊天消息中出现。

示例中的 Agent 会循环调用工具,以获取天气、GitHub 仓库与 Pull Request、加密货币价格、Hacker News 与网页搜索等实时数据,再基于这些信息生成界面。

这一示例展示出生成式 UI 的一种自然形态。

用户不是只从 AI 那里得到一段文本回答。

AI 可以先获取数据,再让数据以卡片、指标、表格、图表、标签页或 3D 场景等形式出现在对话中。

聊天不再只能承载文字。

它也可以成为动态界面的入口。

而这些界面依旧受 Catalog 与 Registry 约束,仍然通过既定组件与真实实现被渲染出来。

从安装开始,搭建自己的生成式 UI

对于 React,可以安装核心包与 React 渲染器:

1
npm install @json-render/core @json-render/react

如果希望使用预构建的 shadcn/ui 组件:

1
npm install @json-render/shadcn

对于 React Native:

1
npm install @json-render/core @json-render/react-native

对于 Vue:

1
npm install @json-render/core @json-render/vue

对于 Svelte:

1
npm install @json-render/core @json-render/svelte

对于 Solid:

1
npm install @json-render/core @json-render/solid

对于视频:

1
npm install @json-render/core @json-render/remotion

对于 PDF:

1
npm install @json-render/core @json-render/react-pdf

对于 HTML 邮件:

1
npm install @json-render/core @json-render/react-email @react-email/components @react-email/render

对于终端界面:

1
npm install @json-render/core @json-render/ink ink react

对于 Next.js 完整应用:

1
npm install @json-render/core @json-render/react @json-render/next

对于 3D 场景:

1
npm install @json-render/core @json-render/react-three-fiber @react-three/fiber @react-three/drei three

从安装到定义 Catalog,再到注册组件和渲染 Spec,json-render 给出的路径并不复杂。

真正重要的是开发者先想清楚:希望 AI 可以使用哪些界面能力,希望它能够触发哪些动作,又希望哪些部分始终掌握在应用自身手中。

生成式 UI 的关键,不只是生成,而是可靠地生成

AI 生成界面,听起来像是一个关于想象力的话题。

但 json-render 的重点并不只是让 AI 更自由地画出页面。

它强调的是可靠性。

AI 只能使用 Catalog 中定义的组件。

JSON 输出需要符合 Schema。

组件实现由开发者提供。

动作由开发者定义。

状态、条件可见性、动态属性与监听器都在结构化规则中运行。

流式输出可以渐进式呈现。

不同平台可以复用同一种生成逻辑。

这让 Generative UI 不只是一个新鲜的展示方式。

它成为一种更具工程边界的界面生成方法。

用户用自然语言表达需求。

AI 在既定规则中生成 JSON。

应用把 JSON 渲染为真实、可交互、可观察的界面。

在这条路径上,AI 的创造力不必与可靠性相互冲突。

开发者不必放弃控制,才能得到动态生成的体验。

json-render 所做的,正是把这两件事放在同一套框架里:让界面能够被生成,也让生成始终知道自己可以走到哪里。