JsonUI
← ConceptsWhy spec-firstThe spec is not documentation. It is the contract the design surface, the engineer, and the agent all sign. Every other artifact — Layout JSON, ViewModel base classes, navigation glue, generated tests — descends from it.~6 min read
The contractA screen_spec declares every uiVariable, every eventHandler, every displayLogic rule, and every customType the screen uses. Nothing about rendering, but everything about what the screen must support. Because it is authored before the layout, designers and engineers argue about behavior in terms of the spec — not in terms of pixel pushing.
counter.spec.json
// docs/screens/json/counter.spec.json
{
"metadata": { "name": "Counter", "platforms": ["web"] },
"stateManagement": {
"uiVariables": [{ "name": "count", "type": "Int", "initial": "0" }],
"eventHandlers": [{ "name": "onIncrement" }]
}
}
The generator then produces a Layout JSON skeleton, a ViewModelBase, a test harness, and per-platform navigation stubs — all deterministically derived. The reader of the spec sees exactly the public surface they can rely on; anything outside that surface is implementation detail and can be rewritten without the spec changing.
The drift gateEvery build runs `jui verify --fail-on-diff`. If the on-disk Layout JSON has drifted from what the spec would generate, the build fails. This one rule is why a spec-first project stays spec-first: the moment someone hand-edits a generated field, the next commit blocks until either the code matches the spec again or the spec is updated to describe the new reality. No silent forking, no stale spec files pretending to be authoritative. What the rule does not promise is coverage: verify compares the screens whose layouts it generates, and since jsonui-cli 1.8.5 it says how many that was — `verified N of M screen(s)`, with the skipped ones and the reason. A project whose layouts are all hand-authored gets a green verify over an empty comparison, so read the denominator before treating the green as evidence.
CI log
$ jui verify --fail-on-diff
 
# Screen | Expected | Actual | Match | Missing | Extra | TypeMM | %
# ─────────────┼──────────┼────────┼───────┼─────────┼───────┼────────┼────
# Counter | 12 | 12 | ✓ | 0 | 0 | 0 | 100%
# Home | 47 | 47 | ✓ | 0 | 0 | 0 | 100%
#
# Total: match=2, missing=0, extra=0, type_mismatch=0
# ✓ No drift detected. Exit 0.
The agent handshakeLLM-driven engineering lives or dies on context. A spec is a compact, machine-readable description of what a screen must do — exactly the context an agent needs. The spec's contract surface now extends to API schemas too: OpenAPI files in `docs/api/` become DTO + Domain layers across all three platforms, and consumers of a shared swagger can scope what gets generated via `api.schemas.{include_paths, exclude_paths, include_schemas, exclude_schemas, skip_domain}` — see /concepts/data-models-from-openapi for the full story. The jsonui-mcp-server exposes the spec + attribute schema via 45 typed tools, so an agent can ask 'what props does a Collection accept?' and get back a structured answer, not a page of prose. The handshake is: the human writes the spec, the agent materializes the implementation within that spec, the drift gate keeps both honest.
agent trace (condensed)
You: implement the submit action on the login screen
 
Agent:
1. mcp.read_spec('login') → gets uiVariables, handlers, transitions
2. mcp.read_layout('login') → confirms Button with onClick="@{onSubmit}"
3. mcp.lookup_attribute('onClick', 'Button') → confirms handler signature: () -> Void
4. edit src/viewmodels/LoginViewModel.ts → implement onSubmit()
5. mcp.jui_build() → zero warnings
6. mcp.jui_verify() → no drift
 
No file reading, no guessing attribute names, no surprises.
What we give upSpec-first is heavier up front. You can't skip straight to 'hack a view and iterate'. Small throwaway tools should probably not adopt it. The trade we are making: forgoing 30 seconds of immediate productivity on day one in exchange for 30 minutes a week of lost debugging, drift-hunting, and re-implementation-per-platform over the life of the project. On a one-week prototype this is a bad trade. On a multi-year product with three platforms and an AI-heavy team, it is the only one that scales.
Keep reading
Data binding as contract@{variable} is not a template expression — it is a typed binding to a ViewModel field./concepts/data-binding
One Layout JSON per screenWhy a single JSON drives all three platforms, and the constraints that choice imposes./concepts/one-layout-json