← GuidesWriting your first specA hands-on walk-through of a complete screen_spec.json. We will author a Counter screen from scratch — check your setup, declare state and handlers, then run the validate / generate / build / verify loop that keeps spec and layout in sync. By the end you will have a spec the generator can turn into Layout JSON and a ViewModelBase — plus a clear sense of which files are yours and which are @generated.~15 min read
0. Before you startThis guide assumes you are inside an existing JsonUI project — one where jui init has already run and docs/screens/json/ exists. Before writing a new spec, it is worth reading one or two of the specs already in the tree. That is how every real spec in this repo got written: copy a neighbour, then change what needs changing.
shell
# Is this a JsonUI project?cat jui.config.json # What specs already exist?find docs/screens/json -name "*.spec.json" | head # Look at one for shape referencecat docs/screens/json/learn/hello-world.spec.json1. Name and platformCreate an empty spec at docs/screens/json/counter.spec.json. metadata.name is the PascalCase component name the generator will produce — the validator rejects anything else (counter or counter_screen both fail fast). metadata.platforms declares which platforms this screen targets — a single-platform doc site is ['web'] here, but the spec is platform-agnostic by design. metadata.layoutFile is the layout filename (without .json) relative to the layouts root.
counter.spec.json
// docs/screens/json/counter.spec.json{ "type": "screen_spec", "version": "1.0", "metadata": { "name": "Counter", "displayName": "Counter", "platforms": ["web"], "layoutFile": "counter" }}2. Declare uiVariablesuiVariables are the ViewModel fields the Layout can bind to via @{name}. Each entry gets a name, a type, and an initial value. Types can be any built-in (Int / String / Bool / Date / etc.) or any customType declared in dataFlow (Step 4). Initial values are written as source code for the target platform; for strings, escape the quotes (\"neutral\"). Note: initial is a free-form string — the validator does not type-check it, the generator pastes it verbatim into the target language.
counter.spec.json (excerpt)
"stateManagement": { "states": [], "uiVariables": [ { "name": "count", "type": "Int", "initial": "0" }, { "name": "statusKey", "type": "String", "initial": "\"neutral\"" } ], "eventHandlers": [], "displayLogic": []}3. Declare eventHandlerseventHandlers are the methods the Layout can invoke via onClick / onChange / onAppear / etc. A handler entry is a name + optional params + a one-line description. The description is the contract between you and the implementer — it tells whoever writes the ViewModel body what each method is supposed to do, in prose the generator will not parse but that reviewers will read. Handler names must match the onX… pattern (onAppear, onSubmit, onCountDidChange) — the validator rejects anything else, making this the most common first-time mistake.
counter.spec.json (excerpt)
"eventHandlers": [ { "name": "onAppear", "description": "Seed initial state." }, { "name": "onIncrement", "description": "count += 1." }, { "name": "onDecrement", "description": "count -= 1 when > 0, no-op otherwise." }]4. Declare customTypesIf the ViewModel needs to expose a shape that is not a built-in type — say, a CounterSnapshot row — declare it under dataFlow.customTypes. The generator will emit a matching TypeScript / Kotlin / Swift type. If the same type is reused across screens, register it in .jsonui-type-map.json so every screen resolves it to the same target.
counter.spec.json (excerpt)
"dataFlow": { "repositories": [], "useCases": [], "apiEndpoints": [], "customTypes": [ { "name": "CounterSnapshot", "properties": [ { "name": "at", "type": "Date" }, { "name": "value", "type": "Int" } ] } ]}5a. ValidateBefore spending time on generation, run the schema validator. `jsonui-doc validate` has two sub-commands: spec (for screen_spec.json files) and component (for component_spec.json files under docs/components/). Each takes the file as a positional argument — there is no `--file`. Exit code 0 means the spec's shape is good enough to hand to the generator; exit 1 lists every violation.
shell
# Validate the spec's shapejsonui-doc validate spec docs/screens/json/counter.spec.json # Exit 0 = ok. Exit 1 = a list of violations.# Component specs use a different sub-command:jsonui-doc validate component docs/components/json/badge.component.json5b. GenerateOnce validate passes, scaffold the per-platform artifacts. The generator writes four kinds of files and is careful about which ones it will overwrite: Layout JSON and *Base stubs get replaced every run, but the hand-authored implementation files (ViewModel, Repository, UseCase) are created only once and then protected. Use --dry-run the first time to see what it would touch without writing anything.
shell
# Scaffold all platforms' layout + base + hookjui generate project --file docs/screens/json/counter.spec.json # Preview without writingjui generate project --file docs/screens/json/counter.spec.json --dry-run # Target a single platformjui generate project --file docs/screens/json/counter.spec.json --web-only5c. Build + verifyRebuild after every spec edit; verify before every commit. jui build regenerates Layouts / Styles / Resources and syncs the per-platform ViewModelBase files. jui verify --fail-on-diff compares the spec with what is actually on disk and refuses to let hidden drift escape into a PR. Verify looks at three independent kinds of drift:• Structural — the components / layout tree in the spec no longer matches the Layout JSON.• Orphan data — Layout JSON has data fields with no matching uiVariables / eventHandlers declaration in the spec.• Unregistered custom types — a type referenced in the spec is not registered in .jsonui-type-map.json.
shell
# Rebuild Layouts / Styles / Resources / ViewModelBase across platformsjui build # Refuse to let drift escape into a PRjui verify --fail-on-diff # Diagnose a single spec with the diff detailjui verify --file docs/screens/json/counter.spec.json --detail6. Know which files are yoursThe generator and hand-written code live in the same tree but follow opposite rules. The short version: anything under /generated/ is @generated and will be overwritten on the next build — edit the spec or the Layout JSON instead. Everything under src/viewmodels/ and src/app/ is yours. Anything with the @generated banner at the top of the file is off-limits.
tree
docs/ screens/ json/ counter.spec.json ← you layouts/ counter.json ← @generated — do not edit Resources/ strings.json ← you (both en and ja)jsonui-doc-web/src/ generated/ ← @generated — do not edit viewmodels/CounterViewModelBase.ts hooks/useCounterViewModel.ts data/CounterData.ts StringManager.ts viewmodels/ ← you CounterViewModel.ts app/counter/ ← you page.tsxTroubleshooting
doc validate-spec fails with 'unknown customType'.Every type referenced in uiVariables / eventHandler params / customTypes.properties must be a built-in OR declared in dataFlow.customTypes. If the type is shared across screens, also add it to .jsonui-type-map.json so cross-screen references resolve.
jui generate project reports 'layoutFile not found'.metadata.layoutFile is the filename (no extension) relative to the layouts root — 'counter' in our example means docs/screens/layouts/counter.json. If your layout lives in a subdirectory, write 'subdir/name' (no leading slash). The generator creates the file on first run if it does not exist, so the 'not found' usually means a typo in metadata.layoutFile rather than a missing file.
jui verify --fail-on-diff keeps failing after every build.Check the diff report: it lists the field that drifted. Two cases: (a) you edited a generated field by hand — either revert the edit or update the spec to describe the new reality; (b) the spec gained a field the generator does not yet implement — file an issue and, if urgent, pin the spec to what the generator supports today. Verify is an integrity check, not an obstacle to work around.
jui generate project says 'Invalid component type: Card'.The built-in type list is fixed (View / Label / Button / Collection / TabView / …). If 'Card' is a custom component, author it first at docs/components/json/card.component.json and add 'Card' to .jsonui-doc-rules.json → rules.componentTypes.screen. Custom components are spec-first in JsonUI — no spec means no type.
jui verify reports 'Orphan data: onSubmit not declared in uiVariables'.Layout JSON has a data entry the spec does not know about. Pick one: add the field to stateManagement.uiVariables (or eventHandlers, if it is a handler) in the spec, or delete the hand-edited data entry from the Layout JSON and let jui build regenerate it. Do not ignore the warning — verify is the only thing that catches generated/hand-written divergence.
Metadata fails validate with 'Name must be PascalCase' or 'ID must be snake_case'.metadata.name is PascalCase (Counter, LoginForm). Component id fields are snake_case (login_submit_btn, counter_value). uiVariables.name is camelCase (isLoading, currentUser). The validator enforces all three — the fix is mechanical, but it catches typos early.
Keep going
Writing layoutsStyles, includes, Collections, bindings — the idioms worth knowing./guides/writing-layouts
Navigation between screensWire onNavigate to router pushes on each platform./guides/navigation
Six ways to split a specOnce the single-file spec you just wrote starts to bulge, the Spec section shows five carve-up patterns to reach for./spec/split-overview