import Link from "next/link"; import { Code } from "@/components/code"; export const metadata = { title: "A2UI Integration | json-render", }; export default function A2UIPage() { return (

A2UI Integration

Use @json-render/core to support{" "} A2UI {" "} natively.

Concept: This page demonstrates how json-render can support A2UI. The examples are illustrative and may require adaptation for production use.

Native A2UI Support

@json-render/core is schema-agnostic. Define a catalog that matches A2UI's format and build a renderer that understands it - no conversion layer needed.

Example A2UI Message

A2UI uses an adjacency list model - a flat list of components with ID references. This makes it easy to patch individual components:

{`{ "surfaceUpdate": { "surfaceId": "main", "components": [ { "id": "header", "component": { "Text": { "text": {"literalString": "Book Your Table"}, "usageHint": "h1" } } }, { "id": "date-picker", "component": { "DateTimeInput": { "label": {"literalString": "Select Date"}, "value": {"path": "/reservation/date"}, "enableDate": true } } }, { "id": "submit-btn", "component": { "Button": { "child": "submit-text", "action": {"name": "confirm_booking"} } } }, { "id": "submit-text", "component": { "Text": {"text": {"literalString": "Confirm Reservation"}} } } ] } }`}

Define the A2UI Catalog

{`import { createCatalog } from '@json-render/core'; import { z } from 'zod'; // A2UI BoundValue schema const BoundString = z.object({ literalString: z.string().optional(), path: z.string().optional(), }).refine(d => d.literalString || d.path); // A2UI children schema const Children = z.object({ explicitList: z.array(z.string()).optional(), template: z.object({ dataBinding: z.string(), componentId: z.string(), }).optional(), }).refine(d => d.explicitList || d.template); export const a2uiCatalog = createCatalog({ components: { Text: { description: 'Displays text content', props: z.object({ text: BoundString, usageHint: z.enum(['h1', 'h2', 'h3', 'body', 'caption']).optional(), }), }, Button: { description: 'Interactive button', props: z.object({ child: z.string(), action: z.object({ name: z.string(), context: z.array(z.object({ key: z.string(), value: BoundString, })).optional(), }).optional(), }), }, DateTimeInput: { description: 'Date/time picker', props: z.object({ label: BoundString.optional(), value: BoundString.optional(), enableDate: z.boolean().optional(), enableTime: z.boolean().optional(), }), }, Column: { description: 'Vertical layout', props: z.object({ children: Children, }), }, Row: { description: 'Horizontal layout', props: z.object({ children: Children, }), }, // Add more A2UI standard components... }, });`}

Define the A2UI Schema

Define the schema for A2UI message types:

{`import { z } from 'zod'; // Component instance in the adjacency list const A2UIComponent = z.object({ id: z.string(), component: z.record(z.record(z.unknown())), }); // Surface update message const SurfaceUpdate = z.object({ surfaceId: z.string().optional(), components: z.array(A2UIComponent), }); // Data model update message const DataModelUpdate = z.object({ surfaceId: z.string().optional(), path: z.string().optional(), contents: z.array(z.object({ key: z.string(), valueString: z.string().optional(), valueNumber: z.number().optional(), valueBoolean: z.boolean().optional(), valueMap: z.array(z.unknown()).optional(), })), }); // Begin rendering message const BeginRendering = z.object({ surfaceId: z.string().optional(), root: z.string(), catalogId: z.string().optional(), }); // Complete A2UI message schema export const A2UIMessage = z.object({ surfaceUpdate: SurfaceUpdate.optional(), dataModelUpdate: DataModelUpdate.optional(), beginRendering: BeginRendering.optional(), deleteSurface: z.object({ surfaceId: z.string() }).optional(), });`}

Build an A2UI Renderer

Create a renderer that processes the A2UI adjacency list format:

{`import { a2uiCatalog } from './catalog'; // Component registry const components = { Text: ({ text, usageHint }) => { const Tag = usageHint?.startsWith('h') ? usageHint : 'p'; return {text}; }, Button: ({ children, action, onAction }) => ( ), DateTimeInput: ({ label, value, onChange }) => ( ), Column: ({ children }) =>
{children}
, Row: ({ children }) =>
{children}
, }; // Render A2UI surface export function renderA2UI( componentMap: Map, dataModel: Record, rootId: string, onAction?: (action: any) => void ) { function resolveBoundValue(bound: any) { if (!bound) return undefined; if (bound.literalString) return bound.literalString; if (bound.path) { const parts = bound.path.replace(/^\\//, '').split('/'); let value = dataModel; for (const p of parts) value = value?.[p]; return value; } } function render(id: string): React.ReactNode { const comp = componentMap.get(id); if (!comp) return null; const [type, props] = Object.entries(comp.component)[0]; const Component = components[type]; if (!Component) return null; // Resolve props const resolved: any = {}; for (const [key, val] of Object.entries(props as any)) { if (key === 'child') { resolved.children = render(val as string); } else if (key === 'children' && val?.explicitList) { resolved.children = val.explicitList.map(render); } else if (val && typeof val === 'object' && ('literalString' in val || 'path' in val)) { resolved[key] = resolveBoundValue(val); } else { resolved[key] = val; } } return ; } return render(rootId); }`}

Usage

{`const [components] = useState(() => new Map()); const [dataModel, setDataModel] = useState({}); const [rootId, setRootId] = useState(null); // Process A2UI messages function handleMessage(msg: any) { if (msg.surfaceUpdate) { for (const comp of msg.surfaceUpdate.components) { components.set(comp.id, comp); } } if (msg.dataModelUpdate) { setDataModel(prev => ({ ...prev, ...msg.dataModelUpdate.contents })); } if (msg.beginRendering) { setRootId(msg.beginRendering.root); } } // Render {rootId && renderA2UI(components, dataModel, rootId, handleAction)}`}

Next

Learn about{" "} Adaptive Cards integration {" "} for another UI protocol.

); }