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.