README
¶
lazy-json
lazy-json is a keyboard-first JSON viewer and editor for the terminal, built with Go and Bubble Tea. It focuses on structured JSON editing instead of raw text editing: move around a tree, expand and collapse nodes, search, edit scalars, add and remove nodes, hand subtrees to your external editor, and optionally run jq transforms without leaving the app.
The project name and keyboard-first workflow are inspired by LazyVim and lazygit: fast navigation, modal interactions, and useful shortcuts over heavyweight UI.
Features
- tree-first navigation with
h/j/k/l,ctrl+b/ctrl+f,gg, andG - prefix shortcuts for clipboard actions, structural jumps, and tree-wide expand/collapse
- ordered object rendering with syntax highlighting, built-in themes, and persistent editor settings
- collapse and expand for objects and arrays
- long arrays expand into 100-item batch rows instead of dumping every element at once
- substring search with
/,n, andN - structured editing for scalars, object keys, object fields, and array items
- Vim-style undo/redo for document changes with
u,U, andctrl+r - configurable pretty-printed JSON saves
- file-backed and stdin-backed sessions
- optional subtree editing through
$EDITOR - optional
jqtransforms through:jqand:jq!
Installation
Install with Homebrew (recommended)
brew tap anyroad/apps
brew install lazy-json
lazy-json --version
You can also install directly without a separate tap step:
brew install anyroad/apps/lazy-json
Install with Go
go install github.com/anyroad/lazy-json@latest
lazy-json --version
Download a release from GitHub
Download the archive for your platform from GitHub Releases, extract it, and place lazy-json somewhere in your PATH.
- macOS and Linux releases are published as
.tar.gz - Windows releases are published as
.zip
Build from a checkout
git clone https://github.com/anyroad/lazy-json.git
cd lazy-json
make build
./dist/lazy-json testdata/basic.json
Build directly with Go
go build -o dist/lazy-json .
./dist/lazy-json testdata/basic.json
Quick Start
Open a file
lazy-json data.json
Open a file with a startup selection
lazy-json --select '$.items[0].name' data.json
Open JSON from stdin
cat data.json | lazy-json
Open stdin with a startup selection
cat data.json | lazy-json --select '$.items[0].name'
Optional tools
jqenables:jqand:jq!transform commands$EDITORenables subtree editing withE
User Guide
Session types
lazy-json starts in one of two modes:
- file-backed:
lazy-json data.json - stdin-backed:
cat data.json | lazy-json
File-backed sessions save back to the original path with :w. Stdin-backed sessions do not have a default file target, so use :w path.json to save to disk or :print to write the current document to stdout.
You can add --select '$.path.to.node' to either startup form to open with a specific node selected. The accepted syntax matches the paths shown in the tree, such as $.items[0].name and $["two words"]. If the full path does not exist, lazy-json falls back to the nearest existing ancestor; if only $ exists, it still opens and shows an error in the footer.
Navigation
j/k: move the selection up or down through visible rowsh: collapse the current container, or move to the parent rowl: expand the current container, or move into the first child- arrays longer than 100 items expand into batch rows such as
[0-99]; uselto open a batch andhto close or leave it ctrl+b/ctrl+forpgup/pgdown: page up or down by the current viewport heightgg/G: jump to the first or last visible row]p: jump to the next parent sibling node, climbing ancestors until a next sibling is foundzR/zM: expand all containers / collapse all containers except the rootza: expand the selected array, or nearest array ancestor, so each container element opens one level; for batched arrays this applies to the selected batch, or the first batch from the array rowzA: collapse the selected array, or nearest array ancestor, so all element containers close?: open the built-in help screent: quick-preview the next theme for the current sessionS: open the settings dialog
Editing model
The editor is structured, not freeform. You operate on the selected node:
u: undo the last document changeU/ctrl+r: redo the last undone document changee: edit the selected scalar value as JSON, such as"text",42,true, ornulla: add a new field to an object or append a new value to an arrayr: rename the selected object keyd: delete the selected nodeE: serialize the selected node or subtree into a temp file, open it in$EDITOR, and replace the node only if the edited JSON parses successfully
Batch rows are navigation-only placeholders for long arrays. You can still press a on a batch row to append to its parent array, but edit, rename, delete, and subtree-copy actions require a real node selection.
Undo and redo track document changes only. Navigation, folds, search query changes, theme previews, and settings changes are not part of edit history. Saving marks the current revision as the saved point instead of clearing history, so undoing back to that revision clears the dirty indicator.
This keeps edits valid and avoids the complexity of embedding a full text editor into the TUI.
Clipboard
yp: copy the selected JSON pathyk: copy the selected object keyyv: copy the selected value as compact JSONys: copy the selected subtree as pretty JSONyj: copy the whole document as pretty JSON
All clipboard copies use structured JSON output rather than the rendered screen text. yv preserves valid JSON scalars and compact containers, while ys and yj use the same current pretty formatting as saves.
Search
/: open searchn: jump to the next matchN: jump to the previous match
Search matches nodes across the whole document based on keys, scalar values, and rendered JSON paths. When n or N lands on a match inside a collapsed subtree or inside a long-array batch, lazy-json automatically expands the necessary ancestors and the matching batch to reveal it.
Commands
:w: save to the current file path:w path.json: save to a specific path:x: save and quit for file-backed sessions; for stdin-backed sessions with no file path, print to stdout and quit:print: print pretty JSON to stdout and quit:q: quit if there are no unsaved changes:q!: quit without saving:undo: undo the last document change:redo: redo the last undone document change:theme: quick-preview the next theme without saving:settings: open the settings dialog:select-path $.items[0].name: select a node by JSON path using the same syntax as--select:copy-path: copy the selected JSON path:copy-key: copy the selected object key:copy-value: copy the selected value as compact JSON:copy-subtree: copy the selected subtree as pretty JSON:copy-json: copy the whole document as pretty JSON:expand-all: expand every object and array in the document:collapse-all: collapse every container except the root:next-parent-sibling: jump to the next sibling of the selected node's parent, climbing ancestors as needed:edit-external: same behavior asE:jq EXPR: apply ajqexpression to the whole document:jq! EXPR: apply ajqexpression to the selected subtree
Settings
Press S or run :settings to open the settings dialog. The dialog currently exposes five rows:
Theme: cycle built-in themes first and then valid external themes from your config directoryLine numbers: toggle a global tree row-number gutter; collapsed rows still count, so visible numbering may have gapsJSON path: toggle whether rendered rows show the selected node pathLong strings: toggle wrapping for displayed string scalar values onlySave indent: choosespaces:2,spaces:3,spaces:4, ortabsfor pretty JSON output
Inside the dialog:
up/downorj/k: move between settings rowsleft/rightorh/l: change the selected settingenter/space: cycle the selected settings: save the current settings tosettings.jsonesc: close the dialog without writing to disk
Theme previews, line-number visibility, JSON-path visibility, and long-string wrapping apply immediately to the current session. The current save-indent setting also applies immediately to later pretty JSON output from :w, :x, :print, ys, and yj. Theme, line-number visibility, JSON-path visibility, wrap, and save-indent changes are not persisted until you press s in the settings dialog, so the quick t / :theme shortcuts remain preview-only switches.
lazy-json stores theme, line-number visibility, JSON-path visibility, wrapping, and save-indent settings under os.UserConfigDir()/lazy-json/settings.json and discovers external themes from os.UserConfigDir()/lazy-json/themes/*.json. The exact base directory follows os.UserConfigDir() for your platform; for example, on Linux this is typically ~/.config/lazy-json/settings.json and ~/.config/lazy-json/themes/.
External theme files are JSON objects with a required name plus optional style slots such as key, string, number, bool, null, muted, selected, search_hit, status, error, border, help, and prompt. Each slot supports foreground, optional background, and optional bold. For example:
{
"name": "mist",
"key": {
"foreground": "#112233"
},
"selected": {
"background": "#ddeeff",
"bold": true
}
}
Examples
Edit a file and save it back:
lazy-json config.json
Pipe JSON in, modify it, then print the result:
cat config.json | lazy-json
Transform a whole document with jq inside the editor:
:jq .items |= map(select(.enabled == true))
Transform just the selected subtree:
:jq! .version = "2"
Save behavior
Pretty JSON output follows the current Save indent setting from the settings dialog. That applies to file saves plus other pretty-output paths such as :print, ys, and yj. The tool still does not preserve the original whitespace layout, and long-string wrapping remains display-only.
Developer Guide
Make targets
make fmt: rewrite Go files withgofmtmake fmt-check: fail if formatting is not cleanmake vet: rungo vet ./...make test: rungo test ./...make perf: run the session/TUI benchmark suite with-benchmemmake perf-save: save the current benchmark baseline to.perf/perf.baseline.txtmake perf-compare: compare current benchmark output against the saved baseline, usingbenchstatwhen availablemake release-check: validate.goreleaser.ymlmake release-snapshot: build snapshot release artifacts locally with GoReleasermake build: builddist/lazy-jsonfor the current platformmake build-all: cross-compile release binaries for the supported target setmake check: run format check, vet, and testsmake clean: removedist/
Local workflow
Recommended local check before pushing:
make check
make build
Release dry-run workflow:
make release-check
make release-snapshot
Benchmark workflow:
make perf-save
make perf-compare
The benchmarks default to:
~/PROJECTS/react-json-view-lite-benchmark/src/hugeArray.json~/PROJECTS/react-json-view-lite-benchmark/src/hugeJson.json
Override them when needed:
LAZY_JSON_BENCH_HUGE_ARRAY=/path/to/hugeArray.json \
LAZY_JSON_BENCH_HUGE_JSON=/path/to/hugeJson.json \
make perf
If you need writable Go cache directories in a restricted environment:
GOCACHE=/tmp/lazy-json-gocache GOMODCACHE=/tmp/lazy-json-gomodcache make check
Badges
README.md uses the live GitHub Actions badge for quality and a committed static SVG for coverage at docs/badges/coverage.svg.
To refresh the coverage percentage manually:
go test ./... -coverprofile=/tmp/lazy-json-coverage.out
go tool cover -func=/tmp/lazy-json-coverage.out | tail -n 1
GitHub Actions
CI: runs on pushes and pull requests targeting thereleasebranch, and executesmake checkplusmake buildRelease: runs when a tag matchingv*is pushed, invokes GoReleaser, uploads GitHub release assets, generates checksums, and updates theanyroad/homebrew-appstap
Create a release tag:
git tag v0.1.0
git push origin v0.1.0
The release workflow expects a HOMEBREW_TAP_GITHUB_TOKEN secret with write access to anyroad/homebrew-apps.
Current Limitations
- saves always rewrite canonical JSON formatting
jqis optional; commands fail cleanly when it is missing$EDITORis optional; external edit fails cleanly when it is not configured
Documentation
¶
There is no documentation for this package.