JsonUI
← Referencejui.config.jsonEvery key the toolchain reads from jui.config.json, in one place: core directories, platform roots, build normalization, API model generation, test install targets, doc contract checks, and lint bounds. Until now these were scattered across the pages that use them.hand-maintained
The file and how it is foundjui.config.json lives at the project root and is the single project-level configuration for the whole toolchain — jui, the platform builders it drives, jsonui-test and jsonui-doc all resolve it. Commands look for it in the current directory and upward; MCP tools take an explicit project_dir (or the JUI_PROJECT_DIR environment variable) instead. `jui init` writes the initial file. One project can hold several of these. When specs sit in one subtree and the config that owns them in another, put a stub on the specs' own ancestry naming its owner — `{ "extends": "../../web/jui.config.json" }` — and the run resolves the API documents and the naming convention from that one file rather than from whichever partial config happens to be nearest (jsonui-cli 1.7.8; see Referencing the API canon from a spec).
Core directoriesThe top-level string keys point the toolchain at your spec, layout, style, image, component-spec and swagger directories. The values below are the built-in defaults — a key you omit falls back to exactly these. strings_file has no default: localization is off until you point it at a strings.json. document_tools_path is only needed when jsonui-doc is not importable from the standard install.⚠️ document_tools_path changes where the tools come from, which the sentence above does not say. Point it at a directory that exists and `document_tools` resolves to that working copy instead of the installed distribution — so `jui --version` no longer describes what is running, and an edit in that copy silently becomes the toolchain. Point it at a directory that does not exist and the setting does nothing at all: the import falls back to the installed CLI. Both cases were silent until jsonui-cli 1.8.58; each now prints a NOTE on stderr naming which of the two happened.The notice is not printed by every command. `jui build` reports the broken case, because a path that does not exist is a fact about the setting and is true no matter what runs — but it stays silent when the path exists, because it never prepends it. A command that did not do the work does not report having done it.
jui.config.json — core keys and their defaults
{
"project_name": "my-app",
"spec_directory": "docs/screens/json",
"layouts_directory": "docs/screens/layouts",
"styles_directory": "docs/screens/styles",
"images_directory": "docs/screens/images",
"component_spec_directory": "docs/components/json",
"strings_file": "docs/screens/layouts/Resources/strings.json",
"type_map_file": ".jsonui-type-map.json",
"api_directory": "docs/api",
"platforms": { }
}
platforms.*One block per shipped platform. `root` is the platform project directory; `layoutsDir` / `imagesDir` are where `jui build` distributes layouts and images inside that root. iOS additionally takes `xcassetsDir`, Android `drawableDir`. A platform without a block simply is not built. The web block below is this documentation site's real configuration.
platforms.* — per-platform roots and distribution dirs
"platforms": {
"ios": {
"root": "ios-app",
"layoutsDir": "Layouts",
"imagesDir": "Images",
"xcassetsDir": "Assets.xcassets"
},
"android": {
"root": "android-app",
"layoutsDir": "app/src/main/assets/Layouts",
"drawableDir": "app/src/main/res/drawable"
},
"web": {
"root": "jsonui-doc-web",
"layoutsDir": "src/Layouts",
"imagesDir": "public/images"
}
}
build.normalizeLayoutsDefault true: `jui build` distributes L1-canonicalized layouts (aliases rewritten to canonical names) and stamps the $jui marker on each distributed file. Set false for byte-identical-to-legacy distribution — the escape hatch, not the recommendation.
build.normalizeLayouts — default true; false is the escape hatch
"build": {
"normalizeLayouts": false
}
api.*Drives `jui g api` (swagger → DTO + Domain models). `api.platforms.<p>` sets the output locations per platform — the values below are the built-in defaults (Android's serializer accepts moshi / kotlinx / none, web's case_convention accepts snake_case / camelCase). `api.schemas` is the endpoint/schema filter: include/exclude path globs pick endpoints, include/exclude schema globs adjust the schema set, and skip_domain names schemas that emit DTOs only. format_mapping opts into OpenAPI format-driven type mapping, with format_mapping_exclude as its per-format opt-out list.
api.* — model generation targets + swagger filter
"api": {
"platforms": {
"ios": { "model_dir": "Model", "dto_subdir": "Generated" },
"android": { "model_package": "model", "dto_subpackage": "generated", "serializer": "moshi" },
"web": { "model_dir": "models", "dto_subdir": "generated", "case_convention": "snake_case" }
},
"schemas": {
"include_paths": ["/api/v1/**"],
"exclude_paths": ["/api/v1/internal/**"],
"include_schemas": ["Shared*"],
"exclude_schemas": [],
"skip_domain": ["*Meta"]
},
"format_mapping": false,
"format_mapping_exclude": []
}
test.*`test.appOwnedScreens` names screens the app implements by hand — a splash or a legal page with no JsonUI layout behind it — so `jui screens` classifies them correctly. They are ids, not layouts, which is also why the spec-coverage check leaves them alone: it looks for layouts with no spec. `test.install` gives jsonui-test its copy targets: when present, a passing `jsonui-test validate` also installs the validated test JSON into each platform's test bundle (iOS additionally takes uitestBundleId for the pre-grant step; a platform value can be a plain path string).
test.* — screen identity + generated-test install targets
"test": {
"appOwnedScreens": ["Splash"],
"install": {
"ios": { "target_dir": "ios-app/UITests/GeneratedTests", "uitestBundleId": "com.example.AppUITests.xctrunner" },
"android": { "assets_dir": "android-app/app/src/androidTest/assets/tests" },
"web": { "dir": "web/e2e/tests" }
}
}
checks[] + databases{}Declarations for `jsonui-doc check` — the doc-contract checker. Each entry has a name, a type (builtin:db-schema, builtin:openapi-diff, or checker for a custom script) and an optional timeout_seconds (default 60). db-schema entries reference a database from databases{} (each declares its dialect) and may pin a dump_command plus ignore_tables; openapi-diff entries must declare impl_openapi_command and may narrow with ignore_paths / ignore_response_codes / scope (all | generated). Since jsonui-cli 1.6.10 they may also set per-comparison-key severity: ignore_schema_keys drops the comparison entirely, downgrade_to_warning keeps the finding with its expected/actual detail but takes it off the gate. Both take names from a closed set — type, nullable, enum, required, format, format_presence — so a typo is a config error rather than a silently ineffective filter, and listing the same key in both is an error too (a dropped comparison cannot be reported). The two format keys stand in an umbrella relation: format_presence covers only annotation differences, where one side declares a format and the other says nothing (docs pinning uuid against a plain string, or an impl declaring email) — the type, the wire value and the generated DTO all agree, so this is docs being deliberately stricter rather than drift. Plain format covers that plus the contradictions, where both sides declare a format and the values disagree (date-time against uuid), which is a genuine type drift. Writing format therefore still means what it meant before the split. The useful setting is downgrade_to_warning: ["format_presence"], which silences the annotation noise while leaving real format contradictions on the gate. Credentials never go in this file — connection info comes from environment variables, and declared commands must resolve inside the project.
checks[] + databases{} — jsonui-doc check declarations
"databases": {
"main": { "dialect": "postgresql" }
},
"checks": [
{
"name": "db-main",
"type": "builtin:db-schema",
"database": "main",
"dump_command": "python scripts/dump_schema.py",
"ignore_tables": ["schema_migrations"]
},
{
"name": "api-diff",
"type": "builtin:openapi-diff",
"impl_openapi_command": "python -m app.export_openapi",
"scope": "generated",
"timeout_seconds": 120
}
]
lint.*Bounds for `jui lint-generated`. lint.max_depth and lint.max_lines can only tighten below the generator's own cut points (depth 5 / 250 lines) — a looser value is clamped with a warning, so the config can never mask what the generators enforce. exclude_dir_names and exclude_files take extra paths out of the scan (extensions/ trees, vendored files).
lint.* — jui lint-generated bounds (tighten-only)
"lint": {
"max_depth": 4,
"max_lines": 200,
"exclude_dir_names": ["extensions"],
"exclude_files": []
}
Keep reading
CLI command referenceThe commands that read every key on this page./reference/cli-commands
DB schema checkWhat checks[] declarations actually verify./concepts/db-schema-check