JsonUI
← Split overviewValidation + drift detectionTwo tools guard specs, and they check different things. `jsonui-doc validate spec` checks the spec file on its own — schema, required fields, cross-references inside the spec. `jui verify` checks drift — whether the on-disk Layout JSON / data section / custom-type registrations match what the current spec would generate. Run both in CI; the combination is what keeps a spec honest end-to-end.~8 min read
Two tools, two jobs`jsonui-doc validate spec` is the spec-side linter. It loads a spec file — or, since jsonui-cli 1.6.25, a directory: every *.spec.json under it is validated in one command, the summary reads `Result: PASSED (N spec file(s))`, failing specs are listed at the end, and the exit code is 1 if any failed (single-file output is unchanged). It walks the schema and reports shape violations (required `metadata.name` missing, malformed `stateManagement.displayLogic` predicate, unknown `type` value, cross-references that don't resolve inside the same spec). It does not read the Layout JSON or the generated code. `jui verify` is the drift detector. It loads every spec in the project, runs it through `LayoutGenerator` to produce what the Layout JSON *should* look like, and compares tree-by-tree to the Layout JSON actually on disk. It also flags `data[]` entries present in the real Layout but missing from `spec.uiVariables`, and custom types referenced in the spec but not registered in `.jsonui-type-map.json`. The two tools are independent commands — neither calls the other — and CI should run validate first (fail fast on schema bugs) then verify (catch drift once the shape is known-good).
What `jsonui-doc validate spec` checksThe validator (`document_tools/.../spec_doc/validator.py`, `SpecValidator` class) dispatches on the top-level `type` field: `screen_spec` / `screen_parent_spec` / `screen_sub_spec` / `component_spec` each have a different required-field set. Inside each, it validates `metadata` (name PascalCase, required description, platforms list), `structure` (component ids unique, `decorativeElements` / `wrapperViews` well-formed, `collection.items` is a valid binding), `stateManagement` (state / uiVariable names match the rules pattern, eventHandler names unique, displayLogic predicates parse), `dataFlow` (viewModel methods/vars consistent, mermaid diagram parses, repositories/useCases return types present), and `userActions` / `transitions` / `validation` / `relatedFiles`. The last step, `_validate_cross_references`, walks the already-validated spec looking for internal references that don't resolve (a transition pointing at an undefined screen, a customType referenced in uiVariables but not declared in `dataFlow.customTypes`).
jsonui-doc validate spec output
$ jsonui-doc validate spec docs/screens/json/learn/hello-world.spec.json
 
Validating docs/screens/json/learn/hello-world.spec.json
type: screen_spec
version: 1.0
 
✗ 3 error(s), 1 warning(s)
 
ERROR metadata.platforms
required — got missing; expected non-empty list of
"ios" / "android" / "web"
 
ERROR structure.components[2].id
duplicate — already used by structure.components[0].id
("learn_hello_world_header")
 
ERROR dataFlow.viewModel.methods.onSelectTab.params[0].type
"string" — did you mean "String"? Type names are
case-sensitive and use the stdlib capitalisation.
 
WARN transitions[0].destination
references "GuidesHome" which does not appear as a screen
in any validated spec in this project.
 
Exit code: 1
What `jui verify` checksThree layers. (1) Layout drift. For every spec, `LayoutGenerator.generate(screen_spec)` produces the Layout tree the spec would emit today, and `ViewDiffChecker.compare(generated, actual)` walks both trees node-by-node. Diffs are reported per-screen. Specs whose `metadata.layoutFile` points at a Layout with no inline `structure.components` are skipped — that's `layoutFile` mode from /spec/layout-file, and generating an empty stub for them would produce a guaranteed false diff. (2) Data-section orphans. After the tree compare, `_diff_data_section` scans the actual Layout's `data[]` for entries not declared in the generated `data[]` (i.e., not in `spec.uiVariables` and not derived from `displayLogic` / `collection` / `tabView`). Any orphan is flagged as a WARNING with name + class — you either add it to `stateManagement.uiVariables` or remove it from the Layout. (3) Unregistered custom types. `_collect_custom_type_refs` walks every type field in the spec (uiVariables, VM method params / returns / vars, repository & useCase signatures) and yields each PascalCase identifier that isn't a stdlib primitive. If that identifier isn't registered in `.jsonui-type-map.json`, it gets flagged so you can add a type-map entry (class + imports). `--fail-on-diff` exits non-zero on layer 1 or 2; layer 3 is WARNING only.
jui verify --fail-on-diff output
$ jui verify --fail-on-diff
 
# Layout drift (layer 1)
─────────────────────────────────────────────────────────
screen: learn/hello-world ✔ no drift
screen: learn/installation ✔ no drift
screen: learn/first-screen ✗ drift detected
at first-screen.spec.root.child[1].child[0]
expected { "type": "Label", "fontSize": 18, "text": "lead" }
actual { "type": "Label", "fontSize": 16, "text": "lead" }
 
Skipped (layout authored externally):
- chrome.spec -> chrome.json
 
# Data-section orphans (layer 2)
─────────────────────────────────────────────────────────
WARNING: 1 data-section entries not declared in spec.uiVariables
across 1 Layout JSON file(s):
- learn/first-screen:
- data.scrollOffset (Int)
→ add each missing entry to stateManagement.uiVariables
(or remove it from the Layout JSON's data section).
 
# Unregistered custom types (layer 3, warnings only)
─────────────────────────────────────────────────────────
WARNING: 1 custom type(s) referenced but not registered in TypeMapper.
Add entries to `.jsonui-type-map.json`:
```json
{
"types": {
"EntryRow": { "class": "EntryRow", "imports": [] }
}
}
```
Usage locations:
- EntryRow: first-screen.spec:uiVariables.entries
 
Exit code: 1 (layer 1 or 2 triggered --fail-on-diff)
Since jsonui-cli 1.6.35, verify also reports what is not there: a screen layout with no spec, and a spec naming a layout that does not exist. That gap had been invisible to every other gate for a structural reason — `jui build` generates from the layout and succeeds without a spec, `verify` compared spec against layout and a screen with no spec never entered the comparison, `validate spec` checks the specs that are present, and the contract-drift job compares the API document against specs. All four compare things that exist, so absence produced no difference anywhere. The check identifies screens through `metadata.layoutFile` — the side that declares the link — and a layout that is not a screen says so itself with `"role": "cell"` on its root rather than being listed in an exclusion file somewhere, since an exclusion list would rot inside exactly the silence this check exists to end. When a finding names something you are not sure about, what settles it is whether the thing has state and an entry point of its own — a screen is arrived at and keeps something; a fragment is drawn inside its parent and keeps nothing the parent does not already own. Asking whether it has its own ViewModel is a fast first read and usually agrees, but it is a proxy: a sheet can have a ViewModel that only re-shelves a dictionary handed down by its parent, which owns neither state nor an entry and so is not a screen. Neither the directory nor a naming convention settles it either — a layout can sit at the top level and still be a fragment, or live under its parent's folder and still be a screen — so it is worth checking rather than inferring. Screens the app implements by hand are outside the check entirely, since they have no layout to be missing a spec for. Findings are warnings by default; `"verify": {"requireSpecPerScreen": true}` in jui.config.json promotes them to a failure once a project has caught up.
jui verify — coverage findings (abridged)
**WARNING: 7 screen layout(s) have no spec:**
- concepts_index
- guides_index
→ author the spec (`jsonui-doc init spec`), or if the layout is not a screen, declare that on
the layout root with `"role": "cell"` so the classification says so rather than a list here
having to.
 
**WARNING: 1 spec(s) name a layout that does not exist:**
- index
→ a rename that moved only one side leaves exactly this. Fix `metadata.layoutFile` or restore
the layout.
(set `"verify": {"requireSpecPerScreen": true}` in jui.config.json to make this fail --fail-on-diff)
Reading the error messagesBoth tools emit `path: message` pairs where *path* is the spec-internal address of the offending field and *message* is the rule that tripped. For validate, paths look like `metadata.name` / `structure.components[2].id` / `dataFlow.viewModel.methods.loadEntries.returnType` — dotted + indexed, rooted at the spec top. For verify, paths include the screen stem and the tree address within the Layout: `login.spec.root.child[1].child[0]` means the Login screen, top-level View's second child, first child of that. When two values appear (`expected … / actual …`), expected is spec-driven (what `LayoutGenerator` emitted) and actual is what's on disk. The fix is almost always on the Layout side (regenerate with `jui build`) unless the spec itself needs to change, in which case update the spec and rerun both tools. For orphans (`data.foo (Int)`), the choice is add-to-spec or remove-from-Layout — no middle ground. For unregistered types, the printed JSON snippet is ready to paste into `.jsonui-type-map.json`; edit the `imports` array to point at the right Swift module / TS path.
Cross-reference checks (parent ↔ sub, customTypes)Cross-reference checks live on both sides of the fence. Validate-side: `_validate_sub_specs` walks a `screen_parent_spec`'s `subSpecs` array, confirming each `{file, name}` entry points at a file that exists and whose `type` is `screen_sub_spec`, and `_parent_spec_has_layout_file` is consulted so subs can legally omit `structure.components` when the parent already supplies `metadata.layoutFile`. Each sub spec is also loaded and its `metadata.parentSpec` cross-checked against the parent's filename. Verify-side: `ParentSpecMerger` collects parent + sub pairs into a single merged spec before generation, and skips sub specs as standalone entries (they're already subsumed by the parent). Custom-type cross-reference lands on the validator's `_validate_cross_references`: any identifier used in a `type` field that isn't a stdlib primitive, not declared in the local `dataFlow.customTypes`, and not registered in `.jsonui-type-map.json` is flagged — you either declare it locally, register it globally, or rename it to a primitive.
CI integrationThe recommended order in CI is validate → verify → build → unit tests. Validate runs first because a malformed spec will crash `LayoutGenerator`, and the resulting traceback is much harder to read than a validator message. Verify runs next with `--fail-on-diff` so any un-regenerated Layout blocks the merge. `jui build` only runs after verify is clean — building on a drifted Layout is legal but hides the drift from the reviewer. Keep the jobs separate so CI's summary line reads `validate ✔ / verify ✔ / build ✔ / test ✔` rather than a single opaque `all-checks ✔`; when one breaks, the bucket name tells you instantly where to look. Caching-wise, validate and verify are Python-only so they share the same setup; `jui build` + tests need Node (for `rjui_tools`) / Ruby (for `sjui_tools`) / Gradle (for `kjui_tools`) depending on target platform.
.github/workflows/spec-guard.yml
# .github/workflows/spec-guard.yml — recommended staging of checks.
# Each bucket fails fast, so CI's summary line reads
# validate ✔ / verify ✔ / build ✔ / test ✔
# rather than a single opaque all-checks ✔.
 
name: spec-guard
on: [pull_request]
 
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.12' }
- run: pip install jsonui-doc
- run: jsonui-doc validate spec docs/screens/json/**/*.spec.json
 
verify:
needs: validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.12' }
- run: pip install jsonui-cli
- run: jui verify --fail-on-diff
 
build:
needs: verify
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: jui build
 
test:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: jsonui-test run
Keep goingThat closes the Spec section. Head back to the map to re-read any pattern, or jump to the test-runner docs for the next layer of CI automation.
Six ways to split a specBack to the map article with the picker cheat sheet./spec/split-overview
Test runnerSpec-driven integration tests — the layer above drift detection, and a natural CI companion to `jui verify`./tools/test-runner