schemaui turns JSON Schema documents into interactive TUI and Web
editors. The terminal path is powered by ratatui, crossterm, and
jsonschema; the browser path ships as an embedded SPA behind the same schema →
form → validate pipeline.
The library parses rich schemas (nested sections, $ref, arrays, key/value
maps, pattern properties…) into a navigable form tree, renders it as a
keyboard-first TUI or a browser form, and validates after every edit so users
always see the full list of issues before saving.
CLI available:
schemaui-cliinstalls theschemauibinary. Prefer the CLI? Jump to CLI installation and usage.
Surfaces at a Glance
| Surface | Who uses it | How to start |
|---|---|---|
| TUI (default CLI mode) | Terminal workflows, SSH, scripts | schemaui -s schema.json -c config.yaml or schemaui tui … |
| Web UI | Browser editing with live JSON preview | schemaui web -s schema.json -c config.yaml |
| Library API | Embed in your Rust app | SchemaUI::run (TUI) or schemaui::web::session (Web) |
Terminal demo (asciinema above) and Web UI (schema tree + form + live JSON side-by-side):
Feature Highlights
- Schema fidelity – draft-07 compatible, including
$ref,definitions,patternProperties, enums, numeric ranges, and nested objects/arrays. - Sections & overlays – top-level properties become root tabs, nested objects are flattened into sections, and complex nodes (composites, key/value collections, array entries) open dedicated overlays with their own validators.
- Immediate validation – every keystroke can trigger
jsonschema::Validator, and all errors (field-scoped + global) are collected and displayed together. - Pluggable I/O –
io::inputingests JSON/YAML/TOML (feature-gated) whileio::outputcan emit to stdout and/or multiple files in any enabled format. - Batteries-included CLI –
schemaui-clioffers the same pipeline as the library, including multi-destination output, stdin/inline specs, and aggregated diagnostics. - Embedded Web UI – enabling the
webfeature bundles a browser UI and exposes helpers underschemaui::web::sessionso host applications can serve the experience without reimplementing the stack.
Config Schema Auto-Detection
When you launch the CLI with --config and omit --schema, schemaui-cli now
resolves the schema in this order:
- Explicit
--schema - A schema declaration embedded in the config document
- Fallback inference via
schema_from_data_value
Supported declarations:
- JSON: root
$schema - TOML:
#:schema ./schema.json - YAML:
# yaml-language-server: $schema=... - YAML fallback:
# @schema ...
Both local and remote schema references are supported. Relative local paths are
resolved against the config file directory, while inline/stdin configs fall back
to the current working directory. JSON $schema metadata is stripped from the
in-memory defaults before validation/output so editor hints do not leak into the
final config payload.
Remote http(s) schema loading exists only in schemaui-cli, and the CLI
enables the remote-schema feature by default. Disable it if you want a
local-only binary surface; the schemaui library crate does not enable any
remote schema loading by default.
Feature defaults are intentionally split by audience:
schemaui-clidefaults to the convenient, batteries-included path: TUI + Web + remote schema loading.schemauidefaults totui + json, so library consumers keep JSON support without pulling in Web or remote/network-related surface area by default.json,yaml, andtomlare real code-level gates. Keep at least one of them enabled; disabling all three triggers a clear compile-time error.
References:
- JSON Schema
$schema: https://json-schema.org/understanding-json-schema/reference/schema - Taplo directives (
#:schema): https://taplo.tamasfe.dev/configuration/directives.html - YAML language server modeline: https://github.com/redhat-developer/yaml-language-server
Quick Start
[]
= "0.12.4"
= "1"
use Result;
use *;
use json;
Public API surface
For library integrations, the main entry points are:
- High-level runtime:
SchemaUI,DocumentInput,FrontendOptions,ServeOptions, andUiOptions - TUI runtime:
crate::tui::session::TuiFrontendfor custom frontend injection viaSchemaUI::run_with_frontend - TUI state:
crate::tui::state::*(for exampleFormState,FormCommand,FormEngine,SectionState) - Schema backend:
crate::ui_ast::build_ui_asttogether withcrate::tui::model::form_schema_from_ui_ast(buildsFormSchemafrom the canonical UI AST)
Architecture Snapshot
┌─────────────┐ parse/merge ┌───────────────┐ layout + typing ┌───────────────┐
│ io::input ├─────────────────▶│ schema ├───────────────────────▶│ tui::state │
└─────────────┘ │ (loader / │ │ (FormState, │
│ resolver / │ │ sections, │
┌─────────────┐ emit Value │ build_form_ │ FormSchema │ reducers) │
│ io::output ◀──────────────────┴────pipeline───┘ └────────┬──────┘
└─────────────┘ focus/edits│
│
┌──────────▼──────────┐
│ tui::app::runtime │
│ (InputRouter, │
│ overlays, status) │
└──────────┬──────────┘
│ draw
┌──────────▼──────────┐
│ tui::view::* │
│ (ratatui view) │
└─────────────────────┘
This layout mirrors the actual modules under src/, making it easy to map any
code change to its architectural responsibility.
Input & Output Design
io::input::parse_document_strconverts JSON/YAML/TOML (viaserde_json,serde_yaml,toml) intoserde_json::Value. Feature flags (json,yaml,toml) keep dependencies lean, and the same gates also driveDocumentFormatparsing/probing at compile time.schema_from_data_value/strinfers schemas from live configs, injecting draft-07 metadata and defaults so UIs load pre-existing values.schema_with_defaultsmerges canonical schemas with user data, propagating defaults throughproperties,patternProperties,additionalProperties,dependencies,dependentSchemas, arrays, and$reftargets without mutating the original tree.io::output::OutputOptionsencapsulates serialization format, pretty/compact toggle, and a vector ofOutputDestination::{Stdout, File}. Multiple destinations are supported; conflicts are caught before emission.OutputOptions::renderturns the finalserde_json::Valueinto JSON/YAML/TOML text, andOutputOptions::writesends that payload to stdout/files explicitly afterSchemaUI::run*returns.
Web UI Mode
Enable the web feature to ship a browser UI (static assets under web/dist/
are embedded in the crate). The same schema/config pipeline used by the TUI
feeds a three-pane experience:
- Navigation – nested schema tree / breadcrumbs for multi-level objects
- Form editor – required flags, enums, numbers, arrays, live field errors
- JSON preview – pretty-printed document that updates as you edit
Screenshot: see Surfaces at a Glance
(docs/web.mix.png).
CLI (fastest path)
schemaui-cli enables web by default. Same I/O flags as TUI; only host/port
are Web-specific:
# random free port on 127.0.0.1; print final JSON to stdout
# fixed port + write result to disk
| Flag | Default | Meaning |
|---|---|---|
--host / -l |
127.0.0.1 |
Bind address (--bind / --listen aliases) |
--port / -p |
0 |
Bind port (0 = ephemeral free port) |
Workflow in the browser: edit fields → Save keeps the session open → Save
& Exit (or Exit) shuts down the temporary server and emits the value through
configured -o destinations.
Full CLI manual: docs/en/cli_usage.md.
Library (embed in your app)
use ;
async
bind_session / serve_session spawn an Axum router with /api/session,
/api/save, /api/exit plus embedded static assets. For custom HTTP stacks,
reuse session_router / WebSessionBuilder instead of the turnkey helpers. The
CLI schemaui web … command is a thin wrapper around these APIs.
Architecture notes:
docs/en/web-ui-architecture-and-refactor-spec.md.
JSON Schema → TUI Mapping
build_ui_ast resolves the schema into the canonical UI AST, and
form_schema_from_ui_ast maps that tree into FormSection/FieldSchema for
the TUI runtime:
| Schema feature | Resulting control |
|---|---|
type: string, integer, number |
Inline text editors with numeric guards |
type: boolean |
Toggle/checkbox |
enum |
Popup selector (single or multi-select for array enums) |
| Arrays | Inline list summary + overlay editor per item |
patternProperties, propertyNames, additionalProperties |
Key/Value editor with schema-backed validation |
$ref, definitions |
Resolved before layout; treated like inline schemas |
oneOf / anyOf |
Variant chooser + overlay form, keeps inactive variants out of the final payload |
Root objects spawn tabs; nested objects become sections with breadcrumb titles.
Every field records its JSON pointer (for example /runtime/http/port) so focus
management and validation can map errors back precisely.
Validation Lifecycle
jsonschema::validator_forcompiles the complete schema once whenSchemaUI::runbegins.- Each edit dispatches
FormCommand::FieldEdited.FormEnginerebuilds the current document viaFormState::try_build_value, runs the validator, and feeds errors back intoFieldStateor the global status line. - Overlays (composite variants, key/value maps, list entries) spin up their own validators built from the sub-schema currently being edited. Nested overlays live on a stack, so each level validates in place before changes flow back to the parent form.
┌─────────────┐ parse schema ┌─────────────────┐ inflate state ┌────────────┐
│ SchemaUI::run├────────────▶│ domain::parse ├───────────────▶│ FormState │
└─────┬───────┘ │ (schema::layout)│ └─────┬──────┘
│ validator_for() └─────────────────┘ edits │
│ ┌──────▼─────────┐
└────────────────────────────────────────────────────── ▶│ app::runtime │
│ (status, input)│
└──────┬─────────┘
│ FormCommand
┌──────▼──────────┐
│ FormEngine │
│ + jsonschema │
└─────────────────┘
App is the sole owner of FormState; even overlay edits flow through
FormEngine so validation rules stay centralized.
TUI Building Blocks & Shortcuts
- Single source for shortcuts –
keymap/default.keymap.jsonlists every shortcut (context, combos, action). Theapp::keymap::keymap_source!()macro pulls this file into the binary,InputRouteruses it to classifyKeyEvents, and the runtime footer renders help text from the same data—keeping docs and behavior DRY. - Root tabs & sections – focus cycles with
Ctrl+J / Ctrl+L(roots) andCtrl+Tab / Ctrl+Shift+Tab(sections). OrdinaryTab/Shift+Tabwalk individual fields. - Fields – render labels, descriptions, and inline error messages. Enum/composite fields show the current selection; arrays summarize length and selected entry.
- Popups & overlays – pressing
Enteropens a popup for enums/oneOf selectors;Ctrl+Epushes a full-screen overlay editor for composites, key/value pairs, and array items. Overlays expose collection shortcuts (Ctrl+N,Ctrl+D,Ctrl+←/→,Ctrl+↑/↓),Ctrl+Ssaves the active level without closing, andEsc/Ctrl+Qpops a single overlay. - Status & help – the footer highlights dirty state, outstanding validation errors, and context-aware help text. When auto-validate is enabled, each edit updates these counters immediately.
Generated shortcut reference
Default context
| Shortcut | Action | Kind |
|---|---|---|
Tab / Down |
Next field | command |
BackTab / Up |
Previous field | command |
Ctrl+Tab |
Next section | command |
Ctrl+Shift+Tab |
Previous section | command |
Ctrl+L |
Next root tab | command |
Ctrl+J |
Previous root tab | command |
Enter |
Open popup / apply selection | command |
Ctrl+E |
Open composite editor | command |
Ctrl+S |
Save & validate (overlays stay open) | command |
Ctrl+Q / Ctrl+C |
Quit (confirm if dirty) | command |
Esc |
Cancel / clear status (overlays: pop current level) | command |
Ctrl+? / Ctrl+H |
Show help and error summary | command |
Collection context
| Shortcut | Action | Kind |
|---|---|---|
Ctrl+E |
Open composite editor | command |
Ctrl+N |
Add entry | command |
Ctrl+D |
Remove entry | command |
Ctrl+Left |
Select previous entry | command |
Ctrl+Right |
Select next entry | command |
Ctrl+Up |
Move entry up | command |
Ctrl+Down |
Move entry down | command |
Ctrl+? / Ctrl+H |
Show help and error summary | command |
Overlay context
| Shortcut | Action | Kind |
|---|---|---|
Tab / Down |
Next field | command |
BackTab / Up |
Previous field | command |
Ctrl+N |
Add entry | command |
Ctrl+D |
Remove entry | command |
Ctrl+Left |
Select previous entry | command |
Ctrl+Right |
Select next entry | command |
Ctrl+Up |
Move entry up | command |
Ctrl+Down |
Move entry down | command |
Ctrl+S |
Save & validate (overlays stay open) | command |
Esc |
Cancel / clear status (overlays: pop current level) | command |
Ctrl+? / Ctrl+H |
Show help and error summary | command |
Popup context
| Shortcut | Action | Kind |
|---|---|---|
Esc |
Close popup | command |
Up |
Select previous popup option | command |
Down |
Select next popup option | command |
Space |
Toggle popup option | command |
Enter |
Apply popup selection | command |
Help context
| Shortcut | Action | Kind |
|---|---|---|
Esc / Ctrl+H / Ctrl+? |
Close help | command |
Tab |
Next error page | command |
BackTab |
Previous error page | command |
Up / k |
Scroll shortcuts up | command |
Down / j |
Scroll shortcuts down | command |
PageUp |
Page shortcuts up | command |
PageDown |
Page shortcuts down | command |
Home |
Jump shortcuts to top | command |
End |
Jump shortcuts to bottom | command |
h |
Scroll error text left | command |
l |
Scroll error text right | command |
Text field context
| Shortcut | Action | Kind |
|---|---|---|
Left |
Move cursor left | local edit |
Right |
Move cursor right | local edit |
Home |
Jump to line start | local edit |
End |
Jump to line end | local edit |
Backspace |
Delete previous character | local edit |
Delete |
Delete next character | local edit |
Ctrl+W |
Delete previous word | local edit |
Ctrl+Z |
Undo text edit | local edit |
Ctrl+Y |
Redo text edit | local edit |
Numeric field context
| Shortcut | Action | Kind |
|---|---|---|
Left |
Step value down | local edit |
Right |
Step value up | local edit |
Shift+Left |
Fast step value down | local edit |
Shift+Right |
Fast step value up | local edit |
Backspace |
Delete previous character | local edit |
Delete |
Delete next character | local edit |
Ctrl+Z |
Undo numeric edit | local edit |
Ctrl+Y |
Redo numeric edit | local edit |
Boolean field context
| Shortcut | Action | Kind |
|---|---|---|
Space / Left / Right |
Toggle boolean value | local edit |
Enter |
Open popup / apply selection | command |
Enum field context
| Shortcut | Action | Kind |
|---|---|---|
Up / Left |
Previous enum option | local edit |
Down / Right |
Next enum option | local edit |
Enter |
Open popup / apply selection | command |
Multi-select field context
| Shortcut | Action | Kind |
|---|---|---|
Enter |
Open popup / apply selection | command |
Composite field context
| Shortcut | Action | Kind |
|---|---|---|
Left |
Previous composite variant | local edit |
Right |
Next composite variant | local edit |
Enter |
Open popup / apply selection | command |
Array buffer field context
| Shortcut | Action | Kind |
|---|---|---|
Backspace |
Delete previous array buffer character | local edit |
Delete |
Clear array buffer | local edit |
Keymap system
Put every shortcut into keymap/default.keymap.json, so runtime logic, help
overlays, and generated README shortcut references all consume a single source
of truth.
-
Format – each JSON object declares an
id, human-readabledescription, bilingualdescriptionZh,contexts(any of"default","collection","overlay","popup","help","text","numeric","boolean","enum","multiSelect","composite","arrayBuffer"), anactiondiscriminated union, and a list of textualcombos. For example: -
Macro + parser –
app::keymap::keymap_source!()include_str!s the JSON,once_cell::sync::Lazyparses it once at startup, and each combo is compiled into aKeyPattern(key code, required modifiers, pretty display string). -
Integration –
InputRouter::classifydelegates tokeymap::classify_key, which returns theKeyActionembedded in the JSON.keymap::help_textfilters bindings byKeymapContext, concatenating snippets used byStatusLineand overlay instructions. -
Generated docs –
build.rsparseskeymap/default.keymap.jsonand refreshes the shortcut blocks inREADME.mdandREADME.ZH.mdusing explicit HTML markers, so normal Cargo builds keep the bilingual reference in sync with runtime behavior. -
Extending – to add a shortcut, edit the JSON, choose the contexts that should expose the help text, and wire the resulting
KeyActioninsideKeyBindingMapif a new semantic command is introduced.
Runtime Layers
| Layer | Module(s) | Responsibilities |
|---|---|---|
| Ingestion | io::input, schema::loader, schema::resolver |
Parse JSON/TOML/YAML, resolve $ref, and normalize metadata. |
| Layout typing | ui_ast::build_ui_ast, tui::model::form_schema_from_ui_ast |
Produce FormSchema (roots/sections/fields) from the canonical UI AST. |
| Form state | tui::state::{form_state, section, field} |
Track focus, pointers, dirty flags, coercions, and errors. |
| Commands & reducers | tui::state::{actions, reducers}, tui::app::validation |
Define FormCommand, mutate state, and route validation results. |
| Runtime controller | tui::app::{runtime, overlay, popup, status, keymap} |
Event loop, InputRouter dispatch, overlay lifecycle, help text, status updates. |
| Presentation | tui::view and tui::view::components::* |
Render tabs, field lists, popups, overlays, and footer via ratatui. |
Each module is kept under ~600 LOC (hard cap 800) to honor the KISS principle and make refactors manageable.
CLI (schemaui-cli)
Install
The installed binary is always named schemaui, so the normal entry point is
schemaui -c ./config.json.
Choose one of the supported channels:
Cargo (cargo install)
Build from crates.io with Cargo.
Cargo binstall
Fetch prebuilt GitHub release binaries through cargo-binstall.
Homebrew
Install from the repository tap on macOS or Linux.
Scoop
Install on Windows from the repository-hosted Scoop manifest.
Direct download
Download the matching archive from
https://github.com/YuniqueUnic/schemaui/releases/latest, extract schemaui /
schemaui.exe, and place it on your PATH.
winget manifests
Use the versioned manifests in packaging/winget with
winget install --manifest <dir>, or submit them upstream to the community
repository.
Modes (subcommands)
The installed binary is schemaui. If you omit a mode subcommand, it defaults
to the TUI. Explicit modes:
| Command | Purpose |
|---|---|
schemaui / schemaui tui |
Interactive terminal editor (default) |
schemaui web |
Interactive browser editor (embedded HTTP server) |
schemaui tui-snapshot |
Precompute TUI FormSchema/layout artifacts (no UI) |
schemaui web-snapshot |
Precompute Web session snapshots JSON/TS (no UI) |
schemaui completion <shell> |
Shell completions (completion feature) |
# equivalent TUI launches
# Web UI (see also Web UI Mode above)
Typical TUI pipeline
┌────────┐ clap args ┌──────────────┐ read stdin/files ┌─────────────┐
│ CLI ├─────────────▶│ InputSource ├─────────────────▶│ io::input │
└────┬───┘ └──────┬───────┘ └────┬────────┘
│ diagnostics │ schema/default Value │
┌────▼─────────┐ ┌──────▼──────┐ |
│Diagnostic │◀───────┤ FormatHint │ │
│Collector │ └──────┬──────┘ │
└────┬─────────┘ │ pass if clean │
│ │ │
┌────▼────────┐ build options └────────────┐ │
│Output logic ├────────────────────────────▶│ OutputOptions │
└────┬────────┘ └────────────┬─────┘
│ SchemaUI::new / with_* ┌───▼────────┐
└──────────────────────────────────────────────▶│ SchemaUI │
│ (library) │
└────────────┘
Shared I/O contract (TUI and Web)
- Inputs –
--schema/--configaccept file paths,file://orhttp(s)URLs (CLIremote-schema), inline payloads, or-for stdin (not both schema and config on stdin at once). Config-only runs infer a schema viaschema_from_data_value(or embedded$schema/#:schema/ YAML modeline). - Diagnostics –
DiagnosticCollectorgathers format issues, feature-flag mismatches, stdin conflicts, and existing output files before the UI runs. - Outputs –
-o/--outputis repeatable and may mix file paths with-for stdout. With no destination, the tool writes to stdout; use--temp-file <PATH>for an explicit fallback file. Extensions pick the format; conflicting extensions are rejected. - Common flags –
--no-pretty(compact),--force/--yes(overwrite),--title/--description(forwarded to the UI). - Web-only flags –
--host/-l(default127.0.0.1),--port/-p(default0= free port). - Shell completion –
schemaui completion <bash|zsh|fish|powershell>(requires thecompletionfeature).
Deep dive: docs/en/cli_usage.md · Chinese:
docs/zh/cli_usage.zh.md.
Key Dependencies
| Crate | Purpose |
|---|---|
serde, serde_json, serde_yaml, toml |
Parsing and serializing schema/config data. |
schemars |
Draft-07 schema representation used by the schema module. |
jsonschema |
Runtime validation for forms and overlays. |
ratatui |
Rendering widgets, layouts, overlays, and footer. |
crossterm |
Terminal events consumed by InputRouter. |
indexmap |
Order-preserving maps for schema traversal. |
once_cell |
Lazy parsing of the keymap JSON. |
clap, clap_complete, anyhow (CLI) |
Argument parsing, shell completion, and ergonomic diagnostics. |
Documentation Map
README.md– overview + architecture snapshot (source of truth).README.ZH.md– Chinese overview kept in sync with this README.docs/en/structure_design.md– detailed schema/layout/runtime design with flow diagrams.docs/zh/structure_design.md– Chinese mirror of the architecture guide.docs/en/cli_usage.md– CLI-specific manual (inputs, outputs, piping, TUI/Web modes, samples).docs/zh/cli_usage.zh.md– Chinese mirror of the CLI usage guide.docs/en/web-ui-architecture-and-refactor-spec.md– Web UI architecture.docs/web.mix.png– Web UI screenshot (schema form + live JSON preview).
Development
- Run
cargo fmt && cargo testregularly; most modules embed their tests byinclude!ing files fromtests/so private APIs stay covered. - Keep modules below ~600 LOC (hard cap 800). Split helpers as soon as behavior grows to keep KISS intact.
- Prefer mature crates (
serde_*,schemars,jsonschema,ratatui,crossterm) over bespoke code unless the change is trivial. - Update
docs/*whenever pipelines, shortcuts, or CLI semantics evolve so user-facing documentation stays truthful.
References
Roadmap
- parse json schema at runtime and generate a TUI
- parse json schema at runtime and generate a Web UI
- parse json schema at compile time Then generate the code for TUI, expose necessary APIs for runtime.
- parse json schema at compile time Then generate the code for Web UI, expose necessary APIs for runtime.
- parse json schema at runtime and generate a Interactive CLI
- parse json schema at compile time Then generate the code for Interactive CLI, expose necessary APIs for runtime.
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Happy hacking!