Dinachi

JSON Render

Render DinachiUI interfaces from JSON specs. Define layouts, forms, and interactive UIs as data — ideal for AI-generated interfaces.

#Overview

JSON Render lets you describe DinachiUI interfaces as JSON and render them as fully accessible components. Write a spec — a flat map of elements with types, props, children, state, and events — and Dinachi renders it.

This is how the Playground works: an AI model outputs a JSON spec, Dinachi renders it live.

bash
npx @dinachi/cli@latest add json-render

Installs the adapter to lib/json-render/, all required UI components, and the @json-render/core, @json-render/react, and zod dependencies.

#Spec Format

A spec is a JSON object with three parts:

json
{
  "root": "myCard",
  "state": {
    "form": { "name": "" }
  },
  "elements": {
    "myCard": {
      "type": "Card",
      "props": { "title": "Hello" },
      "children": ["nameInput", "submitBtn"]
    },
    "nameInput": {
      "type": "Input",
      "props": {
        "label": "Name",
        "value": { "$bindState": "/form/name" }
      }
    },
    "submitBtn": {
      "type": "Button",
      "props": { "label": "Save" },
      "on": {
        "press": {
          "action": "showToast",
          "params": { "title": "Saved!" }
        }
      }
    }
  }
}
FieldDescription
rootID of the top-level element to render
stateInitial state object. Input components bind to paths within this via $bindState.
elementsFlat map of element ID → element definition

Each element has:

FieldDescription
typeComponent name ("Card", "Input", "Box", etc.)
propsComponent props (see reference below)
childrenArray of element IDs rendered inside this element (for container components)
onEvent → action bindings (e.g., { "press": { "action": "navigate", "params": { "url": "/about" } } })

#Rendering a Spec

tsx
"use client";
 
import { Renderer, JSONUIProvider } from "@json-render/react";
import { registry, toastManager } from "@/lib/json-render";
import { Toast } from "@/components/ui/toast";
import type { Spec } from "@json-render/react";
 
export function GenerativeUI({ spec }: { spec: Spec }) {
  return (
    <JSONUIProvider registry={registry} initialState={spec.state ?? {}}>
      <Renderer spec={spec} registry={registry} />
      <Toast toastManager={toastManager} />
    </JSONUIProvider>
  );
}

#State Binding

Use $bindState with a JSON Pointer path to create two-way bindings between components and the spec's state:

json
{
  "state": { "settings": { "theme": "light", "notifications": true } },
  "elements": {
    "themeSelect": {
      "type": "Select",
      "props": {
        "label": "Theme",
        "options": [
          { "label": "Light", "value": "light" },
          { "label": "Dark", "value": "dark" }
        ],
        "value": { "$bindState": "/settings/theme" }
      }
    },
    "notifSwitch": {
      "type": "Switch",
      "props": {
        "label": "Enable notifications",
        "checked": { "$bindState": "/settings/notifications" }
      }
    }
  }
}

The bound prop name depends on the component — value for text inputs/selects, checked for Checkbox/Switch, pressed for Toggle, open for Dialog/Drawer/AlertDialog.

#Events and Actions

Components emit events (like press, change, submit, cancel, confirm). Bind them to actions in the on field:

json
{
  "type": "Button",
  "props": { "label": "Delete", "variant": "destructive" },
  "on": {
    "press": {
      "action": "showToast",
      "params": { "title": "Deleted", "variant": "error" }
    }
  }
}

#Available Actions

ActionParamsDescription
navigateurl, target?Go to a URL. target: "_blank" opens a new tab.
submitformId?Call requestSubmit() on a form element by ID.
showToasttitle, description?, variant?, timeout?Show a toast. Variants: default, success, error, warning.

#Events by Component

EventEmitted by
pressButton
changeInput, Textarea, Checkbox, Switch, Radio, Select, Slider, Toggle, NumberField, ToggleGroup, Tabs
submitInput (on Enter key)
cancelAlertDialog (cancel button)
confirmAlertDialog (action button)

#Validation

Form components support validation via checks and validateOn:

json
{
  "type": "Input",
  "props": {
    "label": "Email",
    "type": "email",
    "value": { "$bindState": "/form/email" },
    "checks": [
      { "type": "required", "message": "Email is required" },
      { "type": "email", "message": "Enter a valid email" }
    ],
    "validateOn": "blur"
  }
}
validateOnBehavior
"change"Validates on every keystroke
"blur"Validates when field loses focus (default for Input, Textarea)
"submit"Validates on form submission

Components that support checks/validateOn: Input, Textarea, Checkbox, Radio, Select, NumberField.


#Component Reference

For the full list of available components and their props, see the Component Reference.


#Full Example

A contact form with validation, state binding, and a toast on submit:

json
{
  "root": "page",
  "state": {
    "form": { "name": "", "email": "", "message": "" }
  },
  "elements": {
    "page": {
      "type": "Box",
      "props": { "gap": "lg", "padding": "lg" },
      "children": ["heading", "card"]
    },
    "heading": {
      "type": "Text",
      "props": { "content": "Contact Us", "variant": "h2" }
    },
    "card": {
      "type": "Card",
      "props": { "title": "Send a Message" },
      "children": ["formFields", "submitBtn"]
    },
    "formFields": {
      "type": "Box",
      "props": { "gap": "md" },
      "children": ["nameInput", "emailInput", "messageInput"]
    },
    "nameInput": {
      "type": "Input",
      "props": {
        "label": "Name",
        "value": { "$bindState": "/form/name" },
        "checks": [{ "type": "required", "message": "Name is required" }]
      }
    },
    "emailInput": {
      "type": "Input",
      "props": {
        "label": "Email",
        "type": "email",
        "value": { "$bindState": "/form/email" },
        "checks": [
          { "type": "required", "message": "Email is required" },
          { "type": "email", "message": "Enter a valid email" }
        ],
        "validateOn": "blur"
      }
    },
    "messageInput": {
      "type": "Textarea",
      "props": {
        "label": "Message",
        "rows": 4,
        "value": { "$bindState": "/form/message" }
      }
    },
    "submitBtn": {
      "type": "Button",
      "props": { "label": "Send Message" },
      "on": {
        "press": {
          "action": "showToast",
          "params": { "title": "Message sent!", "variant": "success" }
        }
      }
    }
  }
}

#Setup API

These are the programmatic APIs for setting up the renderer. Most projects only need the pre-built registry.

tsx
import { registry, toastManager } from "@/lib/json-render";
import type { Spec } from "@json-render/react";

#System prompt generation

Generate a system prompt from the component catalog for AI models:

ts
import { catalog } from "@/lib/json-render";
 
const systemPrompt = catalog.prompt({
  customRules: [
    "Always wrap the root in a Box with padding='lg'",
    "Start with a heading (Text variant='h3')",
  ],
});

For server-side API routes (avoids importing React):

ts
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import {
  dinachiComponentDefinitions,
  dinachiActionDefinitions,
} from "@/lib/json-render/catalog";
 
const catalog = defineCatalog(schema, {
  components: dinachiComponentDefinitions,
  actions: dinachiActionDefinitions,
});
 
const systemPrompt = catalog.prompt({ customRules: [...] });

#Composing with an existing json-render project

If you already have your own json-render catalog, spread Dinachi's definitions in:

ts
import { defineCatalog } from "@json-render/core";
import { schema, defineRegistry } from "@json-render/react";
import {
  dinachiComponentDefinitions,
  dinachiActionDefinitions,
  dinachiComponents,
  dinachiActionHandlers,
} from "@/lib/json-render";
 
const catalog = defineCatalog(schema, {
  components: { ...yourDefs, ...dinachiComponentDefinitions },
  actions: { ...yourActions, ...dinachiActionDefinitions },
});
 
const { registry } = defineRegistry(catalog, {
  components: { ...yourComponents, ...dinachiComponents },
  actions: { ...yourHandlers, ...dinachiActionHandlers },
});

#Overriding a component

ts
import { defineRegistry } from "@json-render/react";
import { catalog, dinachiComponents, dinachiActionHandlers } from "@/lib/json-render";
 
const { registry } = defineRegistry(catalog, {
  components: {
    ...dinachiComponents,
    Button: ({ props, emit }) => (
      <button className="custom-btn" onClick={() => emit?.("press")}>
        {props.label}
      </button>
    ),
  },
  actions: dinachiActionHandlers,
});

#Try It Live

See the Playground to generate DinachiUI interfaces from natural language.