JsonUI
← SpecThe anatomy of a screen specA field-by-field walk through screen_spec.json — every top-level section, what it declares, and how it cross-references the others.~10 min read
Top-level shapeEvery screen_spec.json is the same 11-key shape. Four are required (`type`, `version`, `metadata`, `structure`); the rest are optional but each carries its own scope — declare only what the screen actually uses. The key split below is the mental model the rest of this article walks down into: everything under `metadata` describes *what this screen is*, `stateManagement` and `dataFlow` describe *what it does*, and `structure` describes *what it looks like* (though when a `metadata.layoutFile` is set, `structure` is mostly populated by `layout_importer` at generate time).
screen_spec skeleton
// The skeleton every screen_spec.json starts from. Each top-level key
// is either required (type / version / metadata / structure) or optional.
{
"type": "screen_spec",
"version": "1.0",
"metadata": { /* name, displayName, platforms, layoutFile, … */ },
"structure": { /* components, layout, collection, tabView, … */ },
"stateManagement": { /* states, uiVariables, eventHandlers, displayLogic */ },
"dataFlow": { /* diagram, viewModel, customTypes, repositories, … */ },
"userActions": [ /* what the user taps and what the VM does about it */ ],
"validation": { /* clientSide / serverSide rules */ },
"transitions": [ /* onNavigate(url) → destinations */ ],
"relatedFiles": [ /* Layout / ViewModel / View / Model paths */ ],
"notes": [ /* anything a reviewer needs to know next */ ]
}
metadataThe screen's identity block. `name` is PascalCase and becomes the generated class name across platforms — changing it is a rename refactor, not a cosmetic edit. `displayName` is the human-readable title and `description` is the 1-paragraph summary rendered on the overview of the generated HTML docs. `platforms` drives which outputs `jui build` distributes to. `layoutFile` points at the Layout JSON that owns the hierarchy (see `/spec/split-overview` Pattern 1). `parentSpec` turns this into a `screen_sub_spec` of another screen (Pattern 2). `createdAt` / `updatedAt` are used by the site index to order and age articles.
metadata
{
"metadata": {
"name": "LearnHelloWorld", // PascalCase; becomes the class name
"displayName": "Hello World", // human-readable title
"description": "The five-minute first-screen tutorial.",
"platforms": ["web"], // drives jui build's distribution
"layoutFile": "learn/hello-world", // → docs/screens/layouts/learn/hello-world.json
"parentSpec": "../home.spec.json", // optional — for screen_sub_spec
"createdAt": "2026-04-22",
"updatedAt": "2026-04-24"
}
}
stateManagementFour slots for four different lifecycles. `states` is for explicit persistent state machines and is rarely used. `uiVariables` is the main event — every field the Layout JSON binds through `@{name}` must be declared here with a type and initial expression. `eventHandlers` lists every VM method the Layout binds an `onClick` to. `displayLogic` encodes conditional visibility / enabled rules without putting logic in the Layout — each entry maps a condition on state to effects on an element id, and the generator emits a derived `*Visibility` string the Layout binds to.
stateManagement
{
"stateManagement": {
"states": [], // rarely used; persistent state machines
"uiVariables": [ // everything the Layout binds to
{ "name": "count", "type": "Int", "initial": "0" },
{ "name": "statusVisibility", "type": "String", "initial": "\"gone\"" }
],
"eventHandlers": [ // every handler the Layout binds onClick to
{ "name": "onIncrement", "description": "Bump count; flip statusVisibility." }
],
"displayLogic": [ // derive visibility / enabled from state
{
"condition": "count > 0",
"effects": [{ "element": "status_label", "state": "visible",
"variableName": "statusVisibility" }]
}
]
}
}
dataFlowThe architecture-level slot. `diagram` is a one-line Mermaid flowchart summarising how data moves — rendered inline in the generated HTML. `viewModel.methods` + `viewModel.vars` declare the public contract the hand-written VM must match (if those drift, `jui verify --fail-on-diff` fails). `customTypes` is where reusable row / entry shapes live — see `/spec/split-overview` Pattern 4 for how they cross-reference multiple specs. `repositories` / `useCases` / `apiEndpoints` carry architecture-level slots for data access (often empty for content screens, populated for feature screens).
dataFlow
{
"dataFlow": {
"diagram": "flowchart TD\n VIEW --> VM\n VM -- rows --> VIEW",
"viewModel": { // the public VM contract
"methods": [
{ "name": "onAppear", "description": "Seed rows." },
{ "name": "onSelect", "params": [{ "name": "id", "type": "String" }] }
],
"vars": [
{ "name": "rows", "type": "Array(ActivityRow)", "observable": true }
]
},
"customTypes": [ // reusable shapes (see Pattern 4)
{
"name": "ActivityRow",
"properties": [
{ "name": "id", "type": "String" },
{ "name": "title", "type": "String" }
]
}
],
"repositories": [], "useCases": [], "apiEndpoints": [] // architecture-level slots
}
}
structureThe visual tree. Eight sub-keys, but in practice most screens leave `components` / `layout` empty and let `layout_importer` inline the tree from `metadata.layoutFile` at generate time. `collection` and `tabView` are single-purpose slots only set when the screen is a root Collection page or a root TabView; `collections[]` (plural) is the first-class form for screens with several Collections — each entry is validated individually and gets its cell layouts generated, while tree placement stays with `metadata.layoutFile`. `decorativeElements` and `wrapperViews` annotate purely presentational children and structural wrappers — useful for reviewer communication, not required by the generator. `customComponents` is where you declare this screen uses an externally-specified custom component (see `/spec/split-overview` Pattern 3). `embeds[]` lists sub-screens this layout hosts as regions — each child owns its own ViewModel (see the screen composition concept article).
structure
{
// Most screens leave `structure` mostly empty and let layout_importer
// inline the tree from metadata.layoutFile at generate time.
"structure": {
"components": [], // auto-imported from the Layout JSON
"layout": {}, // same — flat tree of the Layout hierarchy
"collection": null, // only set when the screen is a single-Collection page
"tabView": null, // only set when the screen is a root TabView
"decorativeElements": [], // purely presentational children
"wrapperViews": [], // structural wrappers with a purpose note
"customComponents": [ // see Pattern 3 (component_spec)
{ "name": "CodeBlock",
"ref": "docs/components/json/codeblock.component.json" }
],
"embeds": [ // Screen composition — child owns its own VM
{
"regionId": "detailPane",
"screen": "order_detail",
"params": { "orderId": "@{selectedOrderId}" },
"events": { "onOrderUpdated": "handleOrderUpdated" },
"navigationMode": "delegate"
}
]
}
}
`embeds[]` entries are: `{ regionId, screen, params?, events?, navigationMode? }`. The Layout JSON places each embed via `{ "type": "Embed", "id": <regionId>, ... }`. The embedded screen is unchanged — only the embedding (parent) spec declares the relationship. See /concepts/screen-composition and /reference/components/embed.
Keep goingThe writer-facing tour above lays the ground. The two articles below take you into splitting decisions and the task-focused end-to-end flow.
Six ways to split a specMap article — layoutFile, parent + sub, component_spec, customTypes, cellClasses. Compare, pick the right one for your screen./spec/split-overview
Writing your first specTask-focused 10-step guide: author a screen_spec.json end-to-end for a counter screen./guides/writing-your-first-spec