--- title: BYOC — JSON Render description: Bring your own component library. Have the agent emit a JSON spec and let json-render render it against a Zod-validated catalog of React components. icon: "lucide/Component" snippet_cell: byoc-json-render --- You have a chat surface and you want the agent to draw a dashboard from a typed JSON spec. By the end of this guide, the agent will emit a `{ root, elements }` object, [`@json-render/react`](https://www.npmjs.com/package/@json-render/react) will validate it against a Zod-described catalog, and the user sees the dashboard render as a single React tree. ## When to use this - **Structured UI with a typed contract** where the agent's output is validated against a known schema before it touches the DOM. - **Tolerance for prose preamble + code fences** in the agent's output (json-render's parser handles them). - **Cases where you already use json-render** elsewhere or prefer Zod-validated catalogs. If you'd rather have a streaming progressive render rather than a one-shot validated render, see the sibling page [BYOC — Hashbrown](/generative-ui/hashbrown) for the same scenario with `@hashbrownai/react`. ## Frontend The integration point is ``'s `messageView.assistantMessage` slot. Swap the default renderer for a json-render-backed one: ```tsx title="frontend/src/app/page.tsx" import { CopilotKit, CopilotChat, useConfigureSuggestions, } from "@copilotkit/react-core/v2"; import { JsonRenderAssistantMessage } from "./json-render-renderer"; export default function ByocJsonRenderDemo() { useConfigureSuggestions({ suggestions: [ { title: "Sales dashboard", message: "Show me a sales dashboard." }, { title: "Region breakdown", message: "Break down sales by region." }, ], available: "always", }); return ( ); } ``` The custom renderer parses the streaming assistant content (tolerating partial tokens, code fences, and prose preamble), validates each element against a Zod-typed catalog, and feeds the resulting spec into ``: ```tsx title="frontend/src/app/json-render-renderer.tsx" import { Renderer } from "@json-render/react"; import { catalog } from "./registry"; export function JsonRenderAssistantMessage({ message }: { message: AssistantMessage }) { const spec = parseSpec(message.content ?? ""); if (!spec) return null; return ; } function parseSpec(content: string) { const cleaned = stripCodeFencesAndPrelude(content); const partial = tolerantJsonParse(cleaned); return validateAgainstCatalog(partial); } ``` The catalog lives next to the renderer and pairs each component with a Zod schema describing its props: ```tsx title="frontend/src/app/registry.tsx" import { z } from "zod"; import { MetricCard } from "./metric-card"; import { BarChart } from "./charts/bar-chart"; import { PieChart } from "./charts/pie-chart"; export const catalog = { MetricCard: { component: MetricCard, propsSchema: z.object({ title: z.string(), value: z.number(), delta: z.number().optional(), }), }, BarChart: { component: BarChart, propsSchema: z.object({ data: z.array(z.object({ label: z.string(), value: z.number() })), }), }, PieChart: { component: PieChart, propsSchema: z.object({ data: z.array(z.object({ label: z.string(), value: z.number() })), }), }, }; ``` Validation is the safety net: anything the agent emits that doesn't match a registered schema is rejected before it hits React, so the chat can't render arbitrary garbage. ## Backend The agent emits a `{ root, elements }` JSON object as the assistant message content. `root` references a top-level element id; `elements` maps each id to a `{ type, props, children }` triple matching the catalog. ```json title="example agent output" { "root": "dashboard", "elements": { "dashboard": { "type": "Stack", "children": ["revenue-card", "by-region"] }, "revenue-card": { "type": "MetricCard", "props": { "title": "Total revenue", "value": 184302 } }, "by-region": { "type": "BarChart", "props": { "data": [...] } } } } ``` Anything else (free-form text, code fences around the JSON, a "Here's your dashboard:" preamble) is stripped by the renderer's tolerant parser before validation. The agent doesn't need to be perfectly clean. ## Comparing the two patterns Both `byoc-json-render` and [`byoc-hashbrown`](/generative-ui/hashbrown) solve the same problem with two different rendering libraries. The agent contract is similar; the React glue, validation strategy, and rendering behaviour differ.