Custom Schema & Renderer

Build your own schema and renderer with @json-render/core.

Overview

@json-render/core is schema-agnostic. While @json-render/react provides a ready-to-use schema and renderer, you can create your own to match any JSON structure - whether it's a domain-specific format, an existing protocol, or something entirely custom.

1. Define Your Schema

Start by defining the JSON structure your system will use. Here's an example of a simple dashboard schema:

{
  "layout": "grid",
  "columns": 2,
  "widgets": [
    {
      "type": "metric",
      "title": "Revenue",
      "value": "$12,345",
      "trend": "up"
    },
    {
      "type": "chart",
      "title": "Sales",
      "chartType": "line",
      "dataKey": "salesData"
    },
    {
      "type": "table",
      "title": "Recent Orders",
      "columns": ["id", "customer", "amount"],
      "dataKey": "orders"
    }
  ]
}

2. Create the Catalog

Define a catalog that describes your components and validates props using defineCatalog — see Catalog.

import { defineCatalog } from '@json-render/core';
import { z } from 'zod';

export const dashboardCatalog = defineCatalog(mySchema, {
  components: {
    metric: {
      description: 'Displays a single metric value',
      props: z.object({
        title: z.string(),
        value: z.string(),
        trend: z.enum(['up', 'down', 'flat']).optional(),
        change: z.string().optional(),
      }),
    },
    chart: {
      description: 'Renders a chart visualization',
      props: z.object({
        title: z.string(),
        chartType: z.enum(['line', 'bar', 'pie', 'area']),
        dataKey: z.string(),
        height: z.number().optional(),
      }),
    },
    table: {
      description: 'Displays tabular data',
      props: z.object({
        title: z.string(),
        columns: z.array(z.string()),
        dataKey: z.string(),
        pageSize: z.number().optional(),
      }),
    },
    text: {
      description: 'Displays text content',
      props: z.object({
        content: z.string(),
        variant: z.enum(['heading', 'body', 'caption']).optional(),
      }),
    },
  },
});

3. Define the Root Schema

Create a schema for the overall document structure:

import { z } from 'zod';

const WidgetSchema = z.object({
  type: z.string(),
  title: z.string().optional(),
  // Additional props validated by catalog
}).passthrough();

export const DashboardSchema = z.object({
  layout: z.enum(['grid', 'stack', 'tabs']),
  columns: z.number().optional(),
  widgets: z.array(WidgetSchema),
});

export type Dashboard = z.infer<typeof DashboardSchema>;
export type Widget = z.infer<typeof WidgetSchema>;

4. Build the Renderer

Create a renderer that maps your schema to React components:

import React from 'react';
import { dashboardCatalog } from './catalog';
import type { Dashboard, Widget } from './schema';

// Widget component registry
const widgetComponents: Record<string, React.FC<any>> = {
  metric: ({ title, value, trend, change }) => (
    <div className="p-4 rounded-lg border">
      <p className="text-sm text-muted-foreground">{title}</p>
      <p className="text-2xl font-bold">{value}</p>
      {trend && (
        <p className={`text-sm ${trend === 'up' ? 'text-green-500' : 'text-red-500'}`}>
          {trend === 'up' ? '+' : '-'}{change}
        </p>
      )}
    </div>
  ),

  chart: ({ title, chartType, data }) => (
    <div className="p-4 rounded-lg border">
      <p className="font-medium mb-2">{title}</p>
      <div className="h-48 bg-muted rounded flex items-center justify-center">
        {/* Your chart library here */}
        <span className="text-muted-foreground">{chartType} chart</span>
      </div>
    </div>
  ),

  table: ({ title, columns, data }) => (
    <div className="p-4 rounded-lg border">
      <p className="font-medium mb-2">{title}</p>
      <table className="w-full text-sm">
        <thead>
          <tr>
            {columns.map((col: string) => (
              <th key={col} className="text-left p-2 border-b">{col}</th>
            ))}
          </tr>
        </thead>
        <tbody>
          {data?.map((row: any, i: number) => (
            <tr key={i}>
              {columns.map((col: string) => (
                <td key={col} className="p-2 border-b">{row[col]}</td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  ),

  text: ({ content, variant = 'body' }) => {
    const className = {
      heading: 'text-xl font-bold',
      body: 'text-base',
      caption: 'text-sm text-muted-foreground',
    }[variant];
    return <p className={className}>{content}</p>;
  },
};

// Main renderer
export function DashboardRenderer({
  spec,
  data = {},
}: {
  spec: Dashboard;
  data?: Record<string, any>;
}) {
  const layoutClass = {
    grid: `grid gap-4 ${spec.columns ? `grid-cols-${spec.columns}` : 'grid-cols-2'}`,
    stack: 'flex flex-col gap-4',
    tabs: 'space-y-4',
  }[spec.layout];

  return (
    <div className={layoutClass}>
      {spec.widgets.map((widget, index) => {
        const Component = widgetComponents[widget.type];
        if (!Component) {
          console.warn(`Unknown widget type: ${widget.type}`);
          return null;
        }

        // Resolve data references
        const widgetData = widget.dataKey ? data[widget.dataKey] : undefined;

        return (
          <Component
            key={index}
            {...widget}
            data={widgetData}
          />
        );
      })}
    </div>
  );
}

5. Generate LLM Prompts

Use the catalog to generate system prompts for AI:

const systemPrompt = dashboardCatalog.prompt({
  customRules: [
    'Use metric widgets for single KPI values',
    'Use chart widgets for time-series data',
    'Use table widgets for lists of records',
    'Limit dashboards to 6 widgets maximum',
  ],
});

// Use with any LLM
const response = await generateText({
  model: 'gpt-4',
  system: systemPrompt,
  prompt: 'Create a sales dashboard with revenue, orders, and a chart',
});

6. Validate Specs

Validate incoming specs against your schema. Use catalog.validate() to check AI output against the catalog's Zod schema:

function validateDashboard(spec: unknown) {
  // Validate root structure
  const rootResult = DashboardSchema.safeParse(spec);
  if (!rootResult.success) {
    return { valid: false, errors: rootResult.error.errors };
  }

  // Validate each widget's props against the catalog
  const result = dashboardCatalog.validate(spec);
  if (!result.success) {
    return { valid: false, errors: result.error.errors };
  }

  return { valid: true, errors: [] };
}

Usage Example

'use client';

import { useState } from 'react';
import { DashboardRenderer } from './renderer';
import type { Dashboard } from './schema';

const initialSpec: Dashboard = {
  layout: 'grid',
  columns: 2,
  widgets: [
    { type: 'metric', title: 'Revenue', value: '$12,345', trend: 'up' },
    { type: 'metric', title: 'Orders', value: '156', trend: 'up' },
    { type: 'chart', title: 'Sales Trend', chartType: 'line', dataKey: 'sales' },
    { type: 'table', title: 'Recent Orders', columns: ['id', 'customer', 'amount'], dataKey: 'orders' },
  ],
};

const data = {
  sales: [/* chart data */],
  orders: [
    { id: '001', customer: 'Acme Inc', amount: '$500' },
    { id: '002', customer: 'Globex', amount: '$750' },
  ],
};

export function MyDashboard() {
  const [spec, setSpec] = useState(initialSpec);

  return <DashboardRenderer spec={spec} data={data} />;
}

Next

See how to integrate with A2UI or Adaptive Cards protocols.