JsonUI
← Split overviewParent + sub specsPattern 2 of spec splitting, in detail. One screen, several spec files: since jsonui-cli 1.7.6 the parent is a pure container — it names the sub-specs and little else — and every section that describes behaviour is supplied by the subs. Declaring one of those in the parent is now an error rather than a section that quietly did nothing. Fits screens whose state partitions cleanly by region.~12 min read
When this pattern pays offReach for parent + sub when one screen has (a) 20+ state variables, (b) those variables partition cleanly by region (feed state, notifications state, activity state), and (c) you want per-region code review — a feed team reviewing feed.spec.json without pulling in the notifications team. Don't reach for it to reduce file size alone (that's Pattern 1, layoutFile extraction) or to share shapes across screens (Pattern 4, customTypes).
The parent spec`type: 'screen_parent_spec'` changes the shape, and since 1.7.6 it narrows it. What a parent owns is `type`, `version`, `metadata`, the `subSpecs` roster, `structure.notes`, `relatedFiles` and `notes` — that list is the whole of it. `subSpecs[]` entries are `{ file, name }`, where `file` is a path relative to the parent and `name` matches the child's `metadata.name`. `metadata.layoutFile` still lives here, and the subs inherit it.
chat.spec.json (parent)
// chat.spec.json — the parent. Since 1.7.6 it is a container: a name, a
// roster, and prose. Anything describing behaviour belongs in a sub.
{
"type": "screen_parent_spec",
"version": "1.0",
"metadata": {
"name": "Chat",
"displayName": "Chat",
"description": "Chat screen (composer + streaming transcript).",
"platforms": ["ios", "android", "web"],
"layoutFile": "chat" // subs inherit this
},
"subSpecs": [
{ "file": "chat/chat-core.spec.json", "name": "ChatCore" },
{ "file": "chat/chat-streaming.spec.json", "name": "ChatStreaming" }
],
"structure": { "notes": "Prose about the tree — parent only." },
"notes": ["Parent notes come first in the merged list."]
}
The sub spec`type: 'screen_sub_spec'` plus `metadata.parentSpec` pointing back at the parent; relative paths resolve from the sub-spec's own directory. Everything describing behaviour lives here: `structure.components` and `rootComponents`, the whole of `stateManagement`, `dataFlow` including `viewModel`, `userActions`, `transitions`, `branchContracts`, `unitContracts`, `validation`, `error_handling`, `task_cancellation`. Keep a feature's parts together — a `branchContracts` entry belongs in the same sub-spec as the viewModel method it constrains. Before 1.7.6 that pairing had no legal home at all: `viewModel` only took effect in the parent while `branchContracts` was dropped there, so the two could not sit together anywhere. `unitContracts` was in the same trap until jsonui-cli 1.8.28: the parent refused it and no reader looked in the subs, so a split screen had no legal place to declare one at all.
chat/chat-core.spec.json (sub)
// chat/chat-core.spec.json — one sub. Everything that describes
// behaviour lives here, including the branchContracts for its own
// viewModel methods.
{
"type": "screen_sub_spec",
"version": "1.0",
"metadata": {
"name": "ChatCore",
"description": "Composer + send path.",
"parentSpec": "../chat.spec.json" // relative to this file
},
"stateManagement": { "uiVariables": [ /* … */ ], "eventHandlers": [ /* … */ ] },
"dataFlow": {
"viewModel": { "methods": [ { "name": "send", /* … */ } ] },
"repositories": [ { "name": "ChatRepository", "methods": [ /* … */ ] } ]
},
"branchContracts": [ /* the contracts for send(), beside send() */ ],
"userActions": [ /* … */ ],
"transitions": [ /* … */ ]
}
Inherited vs ownedThe table below is derived from the tool rather than written by hand: each section is placed in a sub-spec, run through the merger, and checked for whether it reaches the merged spec. Three groups. Sub-supplied sections are an error in the parent. `structure.notes` is the reverse — parent only. `relatedFiles` and `notes` accept both and concatenate, parent first. One invariant holds the shape together, and the tool's own tests walk the forbidden list to enforce it: every section the rules forbid in the parent can be supplied from a sub-spec. Being told to move something you cannot move is therefore not a state this can reach — which it did reach once, in 1.7.3, for four of six sections.
where each section goes
Where each section is declared — derived by running the merger, not by hand:
 
section parent sub
------- ------ ---
type / version / metadata / subSpecs only –
structure.notes only –
relatedFiles, notes yes yes (parent first)
structure.components, structure.rootComponents ERROR supplies
stateManagement.uiVariables ERROR supplies
stateManagement.eventHandlers, displayLogic ERROR supplies
dataFlow.viewModel ERROR supplies
dataFlow.repositories, useCases, apiEndpoints ERROR supplies
userActions, transitions ERROR supplies
branchContracts ERROR supplies
error_handling, task_cancellation ERROR supplies
 
metadata.layoutFile and metadata.platforms are declared once, in the
parent, and inherited by every sub.
What jui build doesAt build time the parent's roster drives the merge: the subs supply their sections, the parent supplies its identity, and one merged spec comes out. Sections keyed by name — `viewModel.methods` and `vars`, `repositories`, `useCases`, `apiEndpoints`, `branchContracts`, `unitContracts`, `validation`, `task_cancellation` — are matched across subs: two subs declaring the same thing identically merge to one entry, and two declaring it differently are reported as a conflict, with generation continuing on the first one seen. `error_handling`, `rootComponents`, `userActions` and `transitions` concatenate instead. The conflict is reported by the merger, which means `jui build` sees it and `jsonui-doc validate spec` does not. One correction to that list: `branchContracts` only behaved this way from jsonui-cli 1.7.18. Before it, the merge keyed on the sub-section (`conditions`, `methods`) rather than on the names inside, so a second sub-spec that merely had the section collided with the first and its contracts were dropped whole. Measured on a two-sub fixture whose contracts share no name: v1.7.17 merged one condition and one method and reported two conflicts, v1.7.18 merges both of each and reports none. That a real clash still surfaces is the check worth running — give the two subs the same condition name with different wording and 1.7.18 reports exactly one conflict, keyed on the name. `unitContracts` joined that list in 1.8.28 and is keyed one level deeper still, at (target, case name): several sub-specs may add cases to the same target, which is the normal shape for a split screen, and only the same case name declared differently is a conflict. `validation` followed in 1.8.30, keyed inside each of its two subsections — `clientSide` on `field`, `serverSide` on `condition` — and it had been shipping for a while before anyone asked which of a screen's declared sections actually arrive.
jui build merge flow
// What `jui build` does with a parent + sub group:
//
// 1. Read chat.spec.json → discover subSpecs[] roster
// 2. Reject any behavioural section declared in the parent
// 3. For each sub:
// a. Validate against the screen_sub_spec schema
// b. Resolve parentSpec → inherit layoutFile + platforms
// 4. Merge the subs' sections into one spec:
// keyed sections → matched by name across subs
// identical → one entry
// different → conflict, first one seen wins, build continues
// listed sections → concatenated in roster order
// 5. Generate from the merged spec
//
// A conflict, as the merger words it:
// dataFlow.viewModel.methods[name=send]:
// Defined differently in 'ChatCore' and 'ChatStreaming'
Moving an existing parentIf you have a parent written before 1.7.6, validate it first: the forbidden sections come back as a list, one error per section, each naming what to do with it. Move each section into the sub-spec whose feature it belongs to, then build and read the diff. That last step matters, because the obvious expectation is wrong in a specific way: the generated output before and after is not expected to match. What the parent declared was being discarded, so moving it makes the output grow — on one real screen, uiVariables went from 64 back to 80 and eleven rootComponents took effect for the first time. Deleting those sections instead of moving them would have been the quiet, byte-identical option, and it would have thrown the content away.Three steps, in this order:
migration
1. jsonui-doc validate spec docs/screens/json/chat.spec.json
→ every forbidden section, one error each
 
2. move each section into the sub-spec whose feature it belongs to
→ a branchContracts entry goes beside the method it constrains
 
3. jui build && jui verify
→ read the diff: the output is expected to GROW, not to match
• The error is worded for the move, not for the deletion: `a screen_parent_spec cannot declare 'dataFlow.viewModel' — it is a container, and the merger builds this section from the sub-specs in `subSpecs`. Anything written here is discarded, silently, and the generated code keeps whatever the sub-specs say. Move the declaration into the sub-spec it belongs to.` Since jsonui-cli 1.8.29 the wording branches on whether the section has a destination at all. A section the merger builds from the sub-specs gets the sentence above; one it does not build gets a different ending: `nothing reads this section from a parent. It is NOT built from the sub-specs either, so moving it there would drop it silently: remove it, or add a merge arm for it first.` The distinction matters because the refusal is default-deny while the construction is hand-written, so a section can be refused in the parent and read by nobody in the sub — which happened twice, to `branchContracts` before 1.7.3 and `unitContracts` before 1.8.28. Telling an author to move one of those would send them somewhere the declaration is dropped in silence, which reads as acceptance and is worse than the error they started with.• What made this worth changing is that the old behaviour was invisible from the spec. A parent's list sections were dropped, its `dataFlow.viewModel` won over the subs, and `branchContracts`, `error_handling` and `task_cancellation` were read from neither side — with no warning in any case. On one project nine repository method declarations sat in a parent for months, edited and reviewed, affecting nothing.
When NOT to use itThree anti-patterns. (1) Over-splitting: five+ subs rarely ships — the merge queue becomes the itemneck instead of the file. Two or three subs is the sweet spot. (2) Using it to share a Layout between screens — that's Pattern 1 (two specs referencing the same `layoutFile`), not parent + sub. (3) Using it to share a type across screens — that's Pattern 4 (`dataFlow.customTypes` + `.jsonui-type-map.json`). Parent + sub only makes sense when the thing you're splitting is the *state of a single screen*.
Keep goingTwo patterns covered; three to go.
Separating the layout filePattern 1 — the most common split. Move the visual tree out of the spec and into a Layout JSON./spec/layout-file
Component specsPattern 3 — extract reusable custom UI (CodeBlock, Chart, …) into its own component_spec.json./spec/component-spec