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

<InlineDemo demo="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 `<CopilotChat>`'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 (
    <CopilotKit runtimeUrl="/api/copilotkit-byoc-json-render" agent="byoc_json_render">
      <CopilotChat
        messageView={{ assistantMessage: JsonRenderAssistantMessage }}
      />
    </CopilotKit>
  );
}
```

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 `<Renderer />`:

```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 <Renderer spec={spec} catalog={catalog} />;
}

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.

<IntegrationGrid path="byoc-json-render" />
