The JSON editor that can’t save broken JSON.
A terminal app for viewing and editing JSON — think nano, but it understands JSON structure so you can't accidentally break the formatting.
nanoj is for anyone who hand-edits JSON and is tired of hunting down a missing comma or an unbalanced brace. You navigate and edit a structured view of your data; nanoj writes the file back out as clean, valid JSON every time. Bad formatting isn't caught — it's impossible, because you never edit the raw text directly.
Status: a capable interactive editor. Navigate and edit a collapsible tree or a spreadsheet-style table view; change types, add/cut/copy/paste nodes, search, sort, and save — all backed by a tested valid-by-construction JSON engine. See docs/DESIGN.md.
JSON is everywhere in modern LLM/agent tooling — MCP servers, tool definitions, config. Hand-editing it is error-prone, and a single formatting slip wastes time. nanoj makes well-formed JSON the only possible output.
Requires Go 1.21+.
go build -o nanoj .
./nanoj path/to/file.json
./nanoj --lenient path/to/config.jsonc # tolerate comments + trailing commas
./nanoj --schema schema.json config.json # validate against a JSON Schema
./nanoj --diff old.json new.json # show a structural diff overlay
./nanoj --view data.json # read-only: browse without editing
./nanoj --from yaml config.yaml # convert YAML (or toml) to a JSON working copyBy default nanoj reads strict JSON. Pass --lenient to also accept // and
/* */ comments and trailing commas — handy for opening config-style files.
Because the document model has nowhere to store comments, they are dropped,
and saving writes strict JSON; nanoj shows a ⚠ in the title and a reminder
when you save so this is never a silent surprise. (Trailing commas are simply
normalized away — that's not lossy.) Think of it as "import sloppy JSON and
clean it up" rather than "edit JSONC in place."
Keys (nano-style; ^X means Ctrl-X):
| Key | Action |
|---|---|
↑/↓, Ctrl-P/Ctrl-N |
move |
→ / ← |
expand / collapse (or descend / jump to parent) |
Enter |
edit a value, toggle a bool, or expand/collapse a container |
t |
change a value's type |
a |
add a key (object) or element (array) |
^K / M-6 / ^U |
cut / copy / paste a node (recoverable via paste or undo) |
^W |
search keys and values, or filter by predicate (reveals matches in collapsed branches) |
^T |
toggle the table view for an array of objects |
M-U / M-E |
undo / redo (also ^Z / ^Y) |
^O |
write the file |
^X |
exit (prompts if there are unsaved changes) |
^W does a plain key-or-value substring search by default. For large documents
you can also search by predicate using a small grammar:
| Term | Matches |
|---|---|
key:name |
nodes whose key contains name |
type:number |
nodes of that JSON type (null/bool/number/string/array/object, plus aliases like boolean, int, obj) |
value:123 |
scalar values containing 123 |
key=value |
a field whose key equals key and value equals value (exact, case-insensitive) |
Terms are combined with AND, so key:port type:number jumps to numeric port
fields. A query with no predicate term is treated as a single substring exactly
as before (including queries that contain spaces). In the table view, a cell's
column name acts as its key, so key: and key=value work there too.
When a document is an array of objects, nanoj opens it as a spreadsheet-style
grid (rows = elements, columns = the union of keys). You can also press ^T on
any such array in the tree view. Arrow keys move between cells and Enter
edits a cell: a string/number keeps its type, a bool toggles, and a blank or
null cell is filled with a value whose type is inferred from what you type
(42 → number, true → bool, empty → null, otherwise text). Nested values are
edited in the tree view. ^T or Esc returns to the tree.
Wide tables scroll horizontally as you move: the row-number column stays fixed,
and ‹/› markers in the status bar show when more columns lie off-screen.
^A/^E jump to the first/last column, Home/End to the first/last row, and
PgUp/PgDn page through rows. ^W searches cell values and jumps to the next
match (scrolling it into view). ^K/M-6/^U cut/copy/paste whole rows —
handy for duplicating a row (copy then paste). s sorts the rows by the
selected column (press again to reverse); the sort is type-aware and reorders
the underlying array, so it persists on save and is undoable.
Point nanoj at a JSON Schema and it validates the document and overlays what it learns — without ever touching your data:
nanoj --schema config.schema.json config.json- Values that fail validation are marked with a red
✗in a left gutter. - Required fields are flagged, and an object that is missing a required key reports it in the status line.
- The selected field's expected type and description show in the status line, so the schema doubles as inline documentation.
- When a field is constrained to an enum, pressing
Enteroffers the allowed values as a numbered pick-list instead of free-text entry. - In the table view, annotated cells show their marker inline and the row gutter carries row-level problems (like a missing required key).
It's a read-only overlay: the schema is never written back, and validation
re-runs as you edit so the markers stay live. $ref (including $defs) is
resolved, and the combinators are surfaced per-field where that's unambiguous:
allOf branches all apply, so their properties, required keys, types, and
descriptions are merged; anyOf/oneOf branches contribute the alternative
types (e.g. string or integer) and their enum values merged into one
pick-list. A schema that fails to compile is reported and skipped so it never
blocks editing.
Compare a document against a baseline and see a structured, path-aware diff right in the tree or table:
nanoj --diff baseline.json working.json # opens working.json, compared to baseline.json- Added nodes are marked
+, changed values~; the title bar summarizes the counts ([diff +A~C-R]). - Objects are compared by key and arrays positionally (no move/LCS detection — a reordered array reads as a run of changes).
- Removed nodes don't exist in the working tree, so they can't be marked inline;
they're surfaced as the
-Rcount in the summary.
Like the schema overlay, it's read-only and recomputes as you edit.
nanoj --view data.jsonBrowse and search a document with no chance of changing it: every editing
key and save is disabled and the title shows [read-only]. Handy for
inspecting a file (optionally alongside --schema or --diff) when you only
want to look. Note this is a safety/intent mode — it doesn't meaningfully change
memory use, since undo snapshots are only ever created by edits.
nanoj --from yaml config.yaml # or: --from toml config.tomlThis is an explicit, one-way conversion, not round-trip editing. nanoj
decodes the file into the same JSON tree (key order preserved, big integers
intact, YAML anchors expanded) and opens it as a JSON working copy —
the buffer points at config.json and ^O writes JSON there. The YAML/TOML
source file is never written back, and the title shows a [from yaml] badge
so the mode is never a surprise.
What doesn't survive, by design: comments, anchors/aliases (they're expanded),
and custom tags. Datetimes become RFC 3339 strings. Values JSON genuinely
cannot represent (inf, nan) are a load-time error rather than a silent
mangling. The valid-by-construction promise is a JSON promise — keeping export
out of the editing path is what keeps it honest.
nanoj reads an optional JSON config file (so you can edit it in nanoj itself). Write a starter file to the default location with:
nanoj --write-configThe file is looked up at --config <path>, then $NANOJ_CONFIG, then your
per-OS config dir (e.g. ~/.config/nanoj/config.json on Linux,
~/Library/Application Support/nanoj/config.json on macOS).
Options:
| Field | Values | Meaning |
|---|---|---|
defaultView |
auto (default), tree, table |
which view to open in (auto = table for arrays of objects) |
indent |
a string, e.g. " " or "\t" |
indentation used when saving |
theme |
default, colorblind, high-contrast, mono |
base color/attribute palette |
styles |
per-element overrides | fine-tune individual elements |
Theming is built around accessibility, not just looks:
colorblind— an Okabe–Ito-based palette chosen to stay distinguishable across common forms of color blindness, and it reinforces type differences with bold/italic so meaning never depends on hue alone.high-contrast— bright, bold colors for low-vision use.mono— no color at all; element types are told apart purely by bold, italic, underline, and faint.- The standard
NO_COLORenvironment variable (andNANOJ_NO_COLOR) is honored — when set, nanoj drops all color and falls back to attribute-only styling.
Each element (key, string, number, bool, null, structure, header,
selection) can be individually overridden with fg, bg, bold, italic,
underline, faint, and reverse:
{
"theme": "colorblind",
"styles": {
"string": { "fg": "#3cb371", "italic": true },
"key": { "fg": "12", "bold": true }
}
}nanoj is written in Go for single static binaries across macOS, Windows, Linux, and the BSDs, with a low barrier for contributors. The full reasoning — the "valid by construction" principle, why the tree is the source of truth, and why we lean on the standard-library JSON parser — is in docs/DESIGN.md.
