NAME
schemaui — schemaui turns JSON Schema documents into fully interactive terminal UIs powered by ratatui, crossterm, and…
SYNOPSIS
brew install YuniqueUnic/schemaui/schemauiINFO
DESCRIPTION
schemaui turns JSON Schema documents into fully interactive terminal UIs powered by ratatui, crossterm, and jsonschema. 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 editor, and validates the result after every edit
README
schemaui turns JSON Schema documents into fully interactive terminal UIs
powered by ratatui, crossterm, and jsonschema.
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 editor, and validates the result 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.
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
[dependencies]
schemaui = "0.12.1"
serde_json = "1"
use anyhow::Result; use schemaui::prelude::*; use serde_json::json;fn main() -> Result<()> { let schema = json!({ "$schema": "http://json-schema.org/draft-07/schema#", "title": "Service Runtime", "type": "object", "properties": { "metadata": { "type": "object", "properties": { "serviceName": {"type": "string"}, "environment": { "type": "string", "enum": ["dev", "staging", "prod"] } }, "required": ["serviceName"] }, "runtime": { "type": "object", "properties": { "http": { "type": "object", "properties": { "host": {"type": "string", "default": "0.0.0.0"}, "port": {"type": "integer", "minimum": 1024, "maximum": 65535} } } } } }, "required": ["metadata", "runtime"] });
let config = serde_json::json!({ "metadata": { "name": "demo-service" }, "runtime": { "http": { "host": "127.0.0.1", "port": 8080 } } }); let value = SchemaUI::new(config) .with_schema(schema) .with_title("SchemaUI Demo") .with_description("Edit an existing config against a validation schema") .run(FrontendOptions::Tui(UiOptions::default()))?; println!("{}", serde_json::to_string_pretty(&value)?); Ok(())
}
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
The optional web feature bundles the files under web/dist/ directly into the
crate and exposes high-level helpers for hosting the browser UI. Basic usage:
use schemaui::web::session::{ ServeOptions, WebSessionBuilder, bind_session, };async fn run() -> anyhow::Result<()> { let schema = serde_json::json!({ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "host": {"type": "string", "default": "127.0.0.1"}, "port": {"type": "integer", "default": 8080} }, "required": ["host", "port"] });
let config = WebSessionBuilder::new(schema) .with_title("Service Config") .build()?; let session = bind_session(config, ServeOptions::default()).await?; println!("visit http://{}/", session.local_addr()); let value = session.run().await?; println!("final JSON: {}", serde_json::to_string_pretty(&value)?); Ok(()) }
The helper spawns an Axum router that exposes /api/session, /api/save, and
/api/exit alongside the embedded static assets. Library users can either call
bind_session/serve_session for a turnkey flow or reuse
session_router/WebSessionBuilder to integrate the UI into an existing HTTP
stack. The official CLI (schemaui-cli web …) is merely a thin wrapper around
these APIs.
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:{ "id": "list.move.up", "description": "Move entry up", "descriptionZh": "条目上移", "contexts": ["collection", "overlay"], "action": { "kind": "ListMove", "delta": -1 }, "combos": ["Ctrl+Up"] }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 install schemaui-cli
Cargo binstall
Fetch prebuilt GitHub release binaries through cargo-binstall.
cargo binstall schemaui-cli
Homebrew
Install from the repository tap on macOS or Linux.
brew install YuniqueUnic/schemaui/schemaui
Scoop
Install on Windows from the repository-hosted Scoop manifest.
scoop install https://raw.githubusercontent.com/YuniqueUnic/schemaui/main/packaging/scoop/schemaui-cli.json
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.
schemaui \
--schema ./schema.json \
--config ./defaults.yaml \
-o - \
-o ./config.toml ./config.json
┌────────┐ 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) │
└────────────┘
- Inputs –
--schema/--configaccept file paths, inline payloads, or-for stdin (but not both simultaneously). If only config is provided the CLI infers a schema viaschema_from_data_value. - Diagnostics –
DiagnosticCollectoraccumulates format issues, feature flag mismatches, stdin conflicts, and existing output files before execution. - Outputs –
-o/--outputis repeatable and may mix file paths with-for stdout. When no destination is set, the tool writes to stdout; pass--temp-file <PATH>if you explicitly want a fallback file. Extensions dictate formats; conflicting extensions are rejected. - Flags –
--no-prettytoggles compact output,--force/--yesallows overwriting files, and--title/--descriptionwire through toSchemaUI::with_title/SchemaUI::with_description. - Shell completion –
schemaui completion <bash|zsh|fish|powershell>emits completion scripts from the sameclapcommand graph viaclap_complete.
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, samples).docs/zh/cli_usage.zh.md– Chinese mirror of the CLI usage guide.
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!