BYOC — JSON Render

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.


using Microsoft.Agents.AI;using Microsoft.Extensions.AI;using Microsoft.Extensions.Logging;using OpenAI;/// <summary>/// Factory for the byoc-json-render demo agent.////// Emits a single JSON object shaped like `@json-render/react`'s flat spec/// format (`{ root, elements }`) so the frontend can feed it directly into/// `<Renderer />` against a Zod-validated catalog of three components —/// MetricCard, BarChart, PieChart.////// Mirrors `src/agents/byoc_json_render_agent.py` in the langgraph-python/// showcase — same system prompt, same component catalog./// </summary>public class ByocJsonRenderAgentFactory{    private const int HarnessMaxContextWindowTokens = 128_000;    private const int HarnessMaxOutputTokens = 8_192;    private const string SystemPrompt = @"You are a sales-dashboard UI generator for a BYOC json-render demo.When the user asks for a UI, respond with **exactly one JSON object** andnothing else — no prose, no markdown fences, no leading explanation. Theobject must match this schema (the ""flat element map"" format consumed by`@json-render/react`):{  ""root"": ""<id of the root element>"",  ""elements"": {    ""<id>"": {      ""type"": ""<component name>"",      ""props"": { ... component-specific props ... },      ""children"": [ ""<id>"", ... ]    },    ...  }}Available components (use each name verbatim as ""type""):- MetricCard  props: { ""label"": string, ""value"": string, ""trend"": string | null }  Example trend strings: ""+12% vs last quarter"", ""-3% vs last month"", null.- BarChart  props: {    ""title"": string,    ""description"": string | null,    ""data"": [ { ""label"": string, ""value"": number }, ... ]  }- PieChart  props: {    ""title"": string,    ""description"": string | null,    ""data"": [ { ""label"": string, ""value"": number }, ... ]  }Rules:1. Output **only** valid JSON. No markdown code fences. No text outside   the object.2. Every id referenced in `root` or any `children` array must be a key   in `elements`.3. For a multi-component dashboard, use a root MetricCard and list the   charts in its `children` array, OR pick any element as root and list   the others as its children. Do not emit orphan elements.4. Use realistic sales-domain values (revenue, pipeline, conversion,   categories, months) — the demo is a sales dashboard.5. `children` is optional but when present must be an array of strings.6. Never invent component types outside the three listed above.### Worked example — ""Show me the sales dashboard with metrics and a revenue chart""{  ""root"": ""revenue-metric"",  ""elements"": {    ""revenue-metric"": {      ""type"": ""MetricCard"",      ""props"": {        ""label"": ""Revenue (Q3)"",        ""value"": ""$1.24M"",        ""trend"": ""+18% vs Q2""      },      ""children"": [""revenue-bar""]    },    ""revenue-bar"": {      ""type"": ""BarChart"",      ""props"": {        ""title"": ""Monthly revenue"",        ""description"": ""Revenue by month across Q3"",        ""data"": [          { ""label"": ""Jul"", ""value"": 380000 },          { ""label"": ""Aug"", ""value"": 410000 },          { ""label"": ""Sep"", ""value"": 450000 }        ]      }    }  }}### Worked example — ""Break down revenue by category as a pie chart""{  ""root"": ""category-pie"",  ""elements"": {    ""category-pie"": {      ""type"": ""PieChart"",      ""props"": {        ""title"": ""Revenue by category"",        ""description"": ""Share of total revenue by product category"",        ""data"": [          { ""label"": ""Enterprise"", ""value"": 540000 },          { ""label"": ""SMB"", ""value"": 310000 },          { ""label"": ""Self-serve"", ""value"": 220000 },          { ""label"": ""Partner"", ""value"": 170000 }        ]      }    }  }}### Worked example — ""Show me monthly expenses as a bar chart""{  ""root"": ""expense-bar"",  ""elements"": {    ""expense-bar"": {      ""type"": ""BarChart"",      ""props"": {        ""title"": ""Monthly expenses"",        ""description"": ""Operating expenses by month"",        ""data"": [          { ""label"": ""Jul"", ""value"": 210000 },          { ""label"": ""Aug"", ""value"": 225000 },          { ""label"": ""Sep"", ""value"": 240000 }        ]      }    }  }}Respond with the JSON object only.";    private readonly OpenAIClient _openAiClient;    private readonly ILogger _logger;    public ByocJsonRenderAgentFactory(OpenAIClient openAiClient, ILoggerFactory loggerFactory)    {        ArgumentNullException.ThrowIfNull(openAiClient);        ArgumentNullException.ThrowIfNull(loggerFactory);        _openAiClient = openAiClient;        _logger = loggerFactory.CreateLogger<ByocJsonRenderAgentFactory>();    }    public AIAgent CreateAgent()    {        var chatClient = _openAiClient.GetChatClient("gpt-4o-mini").AsIChatClient();        _logger.LogInformation("ByocJsonRenderAgent constructing harness agent");        // The frontend json-render-renderer.tsx buffers until the assistant        // content parses as a complete JSON object, then renders. The agent        // is steered to emit a single JSON object per reply by the        // SystemPrompt above (carried as the harness ChatOptions.Instructions).        return chatClient.AsHarnessAgent(            HarnessMaxContextWindowTokens,            HarnessMaxOutputTokens,            new HarnessAgentOptions            {                Name = "ByocJsonRenderAgent",                Description = "BYOC json-render flat-spec demo powered by Microsoft Agent Harness over Microsoft Agent Framework.",                ChatOptions = new ChatOptions                {                    Instructions = SystemPrompt,                    MaxOutputTokens = HarnessMaxOutputTokens,                },            });    }}

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

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

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:

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.

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 solve the same problem with two different rendering libraries. The agent contract is similar; the React glue, validation strategy, and rendering behaviour differ.