---
title: "JSON-Render"
description: "JSON-render describes a UI as data: a serializable component spec any frontend renders. Opt-in: a plain devframe pulls zero JSON-render dependencies. Two packages:"
canonical_url: "https://devfra.me/guide/json-render"
---
# JSON-Render

> JSON-render describes a UI as data: a serializable component spec any frontend renders. Opt-in: a plain devframe pulls zero JSON-render dependencies. Two packages:

JSON-render describes a UI as **data**: a serializable component spec any
frontend renders. **Opt-in**: a plain devframe pulls zero JSON-render
dependencies. Two packages:

- **`@devframes/json-render`**: framework-neutral protocol layer (spec/catalog
types, prop schemas, view refs, node runtime), built on
[`@json-render/core`](https://www.npmjs.com/package/@json-render/core).
- **`@devframes/json-render-ui`**: reference Vue frontend implementing the
base catalog with [`@antfu/design`](https://github.com/antfu/design).

## Authoring a view

`createJsonRenderView` registers the spec as shared state, returning a handle:

```ts
import { createJsonRenderView } from '@devframes/json-render/node'

export default defineDevframe({
  /** … */
  setup(ctx) {
    const view = createJsonRenderView(ctx, {
      id: 'metrics', // stable, unique within the scope
      spec: {
        root: 'root',
        elements: {
          root: {
            type: 'Card',
            props: { title: 'Live metrics' },
            children: ['count']
          },
          count: {
            type: 'Text',
            props: {
              text: { $state: '/count' }
            },
            children: []
          },
        },
        state: { count: 0 },
      },
    })

    // A structural change replaces the whole spec…
    view.update(nextSpec)
    // …while state travels as JSON-Pointer patches (only the changed path crosses the wire).
    view.patchState([
      { op: 'replace', path: '/count', value: 3 }
    ])

    // Unregisters the shared state and its listeners.
    // view.dispose()
  },
})
```

The view id is scoped `devframe:json-render:<scope>:<id>`, `scope` defaulting to
the namespace or `global`. Diagnostics fire on invalid props
([DF0038](https://devfra.me/errors/DF0038)), duplicate id ([DF0039](https://devfra.me/errors/DF0039)), disposed-view
use ([DF0040](https://devfra.me/errors/DF0040)), non-serializable spec
([DF0041](https://devfra.me/errors/DF0041)).

## The base catalog

Catalog v1 ships fourteen components: `Stack`, `Card`, `Text`, `Badge`,
`Button`, `Icon`, `Divider`, `TextInput`, `Switch`, `KeyValueTable`,
`DataTable`, `CodeBlock`, `Progress`, `Tree`. A spec **is** an `@json-render/core`
`Spec` plus a per-component Zod prop schema (`basePropSchemas`), validated at
ingress (node side) and render time (browser side); `$state` / `$bindState`
bindings work on scalar props.

## Actions and state

- **State** is a JSON-serializable `Record<string, unknown>` addressed by JSON
Pointer.
- **Actions** are unrestricted: an element event dispatches an RPC call of the
same name, with no allowlist.
- **Reserved built-ins** (`setState`, `pushState`, `removeState`,
`validateForm`) are handled client-side, never bridged to RPC.

## Rendering standalone

### Out-of-box SPA (no client build)

`createJsonRenderDevframe` points `clientAssets` at the prebuilt
`@devframes/json-render-ui/spa` renderer:

```ts
import { createJsonRenderDevframe } from '@devframes/json-render-ui/spa'
import { createJsonRenderView } from '@devframes/json-render/node'

export default createJsonRenderDevframe({
  id: 'my-app',
  name: 'My App',
  version,
  packageName,
  homepage,
  description,
  cli: { command: 'my-app', port: 9800 },
  setup(ctx) {
    createJsonRenderView(ctx, { id: 'main', title: 'Dashboard', spec })
  },
})
```

The SPA discovers views from the **view index** (`JSON_RENDER_INDEX_KEY`); one
view renders full-bleed, multiple get a `title`-labeled switcher.

### Custom frontend

A custom frontend renders from shared state: connect with `connectDevframe()`,
read the view's state (keyed `devframe:json-render:<scope>:<id>`), subscribe to
`updated` events, and render with your registry; the [Next
hub example](https://github.com/devframes/devframe/tree/main/examples/custom-hub-next) has a React renderer. In a
**static** build spec + state are read-only: actions unavailable, local
state and bindings still work.

### The reference frontend

`@devframes/json-render-ui` ships prebuilt `/spa` and `/hub` bundles.

## Rendering inside a hub

`@devframes/json-render/hub` adds a `json-render` dock type:

```ts
// node side: register a dock entry carrying the view's serializable reference,
// and compose the frontend as a prebuilt renderer module
import { jsonRenderUiRenderer } from '@devframes/json-render-ui/hub'
import { toJsonRenderDockEntry } from '@devframes/json-render/hub'

initHub({
  ui: createUi(),
  renderers: [
    jsonRenderUiRenderer()
  ],
  configure(ctx) {
    ctx.docks.register(toJsonRenderDockEntry(view, {
      id: 'metrics',
      title: 'Metrics',
      icon: 'ph:chart-bar-duotone',
    }))
  },
})
```

The hub publishes the module in the [renderer
manifest](https://devfra.me/guide/hub-initiate#renderer-modules), lazily imported on first mount (else a
missing-renderer fallback). A host page can register a
`JsonRenderDockRenderer` **locally** instead, overriding the manifest:

```ts
// host page: a locally-bundled frontend wins over the manifest module
import { createDevframeClientRuntime } from '@devframes/hub/client'
import { myJsonRenderDockRenderer } from './my-renderer'

const host = await createDevframeClientRuntime({
  renderers: { 'json-render': myJsonRenderDockRenderer },
})

// the hub UI provider mounts the active dock entry into a container it owns
const result = await host.context.renderers.mount(entry, container)
if (result.status === 'mounted')
  result.dispose // tear down when the hub UI provider decides; deactivation disposes too
```

The dock entry carries a serializable `JsonRenderViewRef` in two shapes: `{ stateKey }`
points at live shared state (`createJsonRenderView`); `{ spec }` embeds it inline
for a browser-synthesized [client-only dock](https://devfra.me/guide/client-context#client-only-docks).

## Swapping the frontend

`@devframes/json-render/hub` exports the contract, `JsonRenderDockRenderer` and
`JsonRenderDockMountOptions`; `@devframes/json-render-ui` is the pluggable
reference implementation.
See [Build your own JSON-Render frontend](https://devfra.me/guide/build-your-own-json-render-frontend)
and the [`json-render` example](https://github.com/devframes/devframe/tree/main/examples/json-render).


## Sitemap

See the full [sitemap](https://devfra.me/sitemap.md) for all pages.
