Skip to content
Forms, switch product

Form Chunks

A JSON form definition does not have to live in a single file. A widget entry written as { "$ref": "./tabs/alert.form-chunk.json" } is a chunk reference: it names another JSON file that holds the real widget. Before rendering, you call resolveChunkRefs. It fetches every reference, puts the loaded widget in its place, and returns a plain form definition that <gui-form> accepts.

This is a JSON API feature. On the Programmatic API you split a form definition with ordinary ES module imports.

A form definition grows with the form. A single file of a few thousand lines is hard to review, hard to diff, and slow to validate. Chunks let one file describe one section, so each file stays small enough to read on its own and a change to one section produces a one-file diff.

A tabbed form is the clearest case. The parent file declares the tabs layout and one chunk reference per tab, and each tab lives in its own file. Adding a tab then means adding a file and one line, instead of editing a file that holds every tab at once.

The parent is an ordinary form definition. A chunk reference goes anywhere a widget can go:

{
"$schema": "https://golemui.com/schemas/form.schema.json",
"form": [
{
"kind": "layout",
"type": "tabs",
"props": {
"defaultOpen": "tabAlert",
"tabs": [
{ "label": "Alert Component", "uid": "tabAlert" },
{ "label": "Button Component", "uid": "tabButton" }
]
},
"children": [
{ "$ref": "./tabs/alert.form-chunk.json" },
{ "$ref": "./tabs/button.form-chunk.json" }
]
}
]
}

$ref must be the object’s only key. An object that carries $ref plus any sibling key is treated as a normal widget and is not resolved.

A chunk file holds one widget object, not an array. It can declare its own $schema so an editor validates it on its own:

{
"$schema": "https://golemui.com/schemas/gui/layout-widget.schema.json",
"uid": "tabAlert",
"kind": "layout",
"type": "flex",
"children": [
{
"kind": "display",
"type": "alert",
"props": { "text": "Check your details", "level": "warning" }
}
]
}

resolveChunkRefs strips $schema from every chunk it loads, because the form decoder does not expect it. The parent file keeps its own $schema.

A chunk can contain chunk references of its own, and they are resolved too.

resolveChunkRefs is exported from @golemui/gui-shared:

function resolveChunkRefs(
formJson: Record<string, unknown>,
baseUrl: string,
): Promise<Record<string, unknown>>;

It takes the parsed parent form and the URL the references are resolved against. It returns a new object and does not modify the input. Fetch the form, resolve it, then pass the result as formDef:

load-form.ts
import { resolveChunkRefs } from '@golemui/gui-shared';
export async function loadForm(path: string) {
const baseUrl = new URL(path, window.location.href).href;
const json = await fetch(baseUrl).then((response) => response.json());
return resolveChunkRefs(json, baseUrl);
}

The form definition now arrives after an await, so the component has to render nothing until it is there:

import { useEffect, useState } from 'react';
import { GuiForm } from '@golemui/gui-react';
import { loadForm } from './load-form';
export function MyForm() {
const [formDef, setFormDef] = useState(undefined);
useEffect(() => {
loadForm('/forms/settings.form.json').then(setFormDef);
}, []);
if (!formDef) {
return null;
}
return <GuiForm config={{ formDef }} />;
}

Every reference is resolved with new URL(ref, baseUrl), so it follows the same rules as a URL in a web page. Relative paths (./tabs/alert.form-chunk.json), root-relative paths (/forms/tabs/alert.form-chunk.json), and absolute URLs (https://cdn.example.com/alert.json) all work.

Pass the form document’s own absolute URL as baseUrl. A loaded chunk then resolves its own references against its own URL, not against the parent’s. A chunk directory can move as a unit without rewriting the references inside it.

The chunk files must be reachable over HTTP at the resolved URL, because the resolver fetches them.

This means import formDef from './my-form.json' does not work for a chunked form. A bundler inlines the parent file and never sees the references. Put the parent form and its chunks in your static asset directory instead, and load the parent with fetch.

  • Only the top-level form array and children arrays are traversed. A reference inside props, such as a repeater template, is not resolved.
  • JSON pointers are not supported. A #/... fragment is ignored and the whole document is inlined.
  • There is no cache and no de-duplication. The same reference used twice causes two requests.
  • There is no cycle detection. A chunk that refers back to its own parent recurses until the call stack overflows.
  • Resolution is eager. Every reference is fetched before the form renders, siblings in parallel. A chunk is not fetched when its tab opens.
  • One failed fetch rejects the whole call with Failed to load form chunk <url>: <status>. There is no partial result, so handle the rejection where you call loadForm.

A chunk reference is one of the accepted shapes of a form widget in the published schemas, so it validates anywhere a widget can appear. The MCP tool json_validate_form_definition accepts one for the same reason. A chunk file itself validates against the schema it names in its own $schema.