JSONBeautify JSON Linter

The JSON Cheatsheet: This Tool, jq, and JSONPath on One Screen

JSONBeautify — Free Online JSON Formatter, Validator & Minifier Guides · Updated 2026-10-01 · All guides

One screen. Print it or pin the tab. Everything here was run on jq 1.7 and Node 24 before it was written, so the outputs are real, not remembered.

This tool, honestly

Two panes: paste on the left, formatted output on the right. Validation runs on every keystroke — no submit button, no upload, nothing leaves the browser.

ControlWhat it does
Format (2 spaces)JSON.stringify(parsed, null, 2)
Format (4)Same, four-space indent
MinifyJSON.stringify(parsed) — no whitespace
Auto-RepairSingle quotes → double, drops trailing commas, quotes naked keys
Sample / Clear / CopyDemo payload, empty both panes, copy output

The status bar tells you array length and byte size; the footer reports line and character counts. On failure, the output pane carries the engine's own error text — V8's position N, which you convert with one line:

const m = err.message.match(/position (\d+)/);
const upto = raw.slice(0, +m[1]);
const line = upto.split("\n").length, col = upto.length - upto.lastIndexOf("\n");

There are no keyboard shortcuts. No Ctrl+Enter, no key handlers of any kind — buttons only. Say so plainly rather than let you hunt for one. Auto-Repair is a diagnostic, not a pipeline: three regex substitutions will happily rewrite an apostrophe inside a value. Fix the source. For the deeper null vs missing, number-precision, and date-format decisions, see JSON best practices.

jq: twelve one-liners

Sample document, so the outputs below are reproducible:

{"users":[{"id":1,"name":"ada","role":"admin","tags":["math","eng"],"joined":"2024-01-05"},
          {"id":2,"name":"linus","role":"dev","tags":["kernel"],"joined":"2023-06-11"},
          {"id":3,"name":"grace","role":"admin","tags":["navy","compilers"],"joined":"2025-02-20"}],
 "meta":{"total":3,"page":1}}
# 1. Project — pick fields
jq -c '[.users[] | {id, name}]'                         # [{"id":1,"name":"ada"},…]

# 2. Filter
jq -c '[.users[] | select(.role=="admin") | .name]'     # ["ada","grace"]

# 3. Delete a key everywhere it appears
jq -c 'del(.. | select(has("tags")?) | .tags)'          # tags gone at any depth

# 4. Rename a key (jq has no rename; map the entries)
jq -c '.users |= map(with_entries(if .key=="joined" then .key="joinedAt" else . end))'

# 5. Every scalar path, dotted
jq -c 'paths(scalars) | join(".")'                      # users.0.id, users.0.name, …

# 6. Where is this value? (path of every match)
jq -c 'paths(scalars) as $p | select(getpath($p)=="admin") | $p | join(".")'
#                                                        # users.0.role, users.2.role

# 7. "grep" for any string matching a pattern
jq -c '.. | strings | select(test("kernel"))'            # "kernel"

# 8. Count leaves — a fast shape summary
jq -c '[paths(scalars)] | length'                       # 19

# 9. Group and count
jq -c '.users | group_by(.role) | map({role: .[0].role, n: length, names: [.[].name]})'

# 10. Distinct values across a nested array
jq -c '[.users[].tags] | flatten | unique'               # ["compilers","eng","kernel","math","navy"]

# 11. Argument-injected filter (no shell interpolation, no injection)
jq -c --arg r admin '[.users[] | select(.role==$r) | .id]'   # [1,3]

# 12. Transform every string, recursively
jq -c '.users |= walk(if type=="string" then ascii_downcase else . end)'

Two traps cost people the most hours: group_by needs the array, so .users | group_by(.role) works and group_by(.role) on the root object throws Cannot index array with string "role". And input_line_number aborts on the first malformed NDJSON line rather than reporting it — use jq -c . file 2>&1 to see which line broke.

JSONPath: the same tasks, minus the ones it can't do

JSONPath (RFC 9535) is a selector language. It selects. It does not reshape, rename, delete, or aggregate — implementations bolt those on and disagree about how.

TaskjqJSONPath
All names[.users[].name]$..name
Filter equalselect(.role=="admin")$.users[[email protected]=='admin'].name
Filter numericselect(.id>1)$.users[[email protected]>1].id
Wildcard children.users[*]$.users[*]
Slice.[0:2]$.users[0:2:1]
Union// alternative$.users[0,2].name
Regexselect(.name|test("^a"))$.users[[email protected]=~'^a']
Parent of match.. | select(.=="x")$..* + host code
Delete / rename / groupdel, with_entries, group_bynot in the language

Use JSONPath when you're reading — a config explorer, a test assertion, a UI's data binding. Use jq the moment you need to compute. Full comparison in JSON tools compared.

Validate or crash

Fail the build, fail the deploy, fail the CI step — before the payload reaches a client.

# jq: exit 0 valid, nonzero invalid, -e keeps output off stdout
jq -e . config.json > /dev/null || exit 1

# Python stdlib, no third-party imports
python3 -m json.tool config.json > /dev/null || exit 1

# Node, no dependencies — reads stdin, exit 1 on bad JSON
node -e 'JSON.parse(require("node:fs").readFileSync(0,"utf8"))' < config.json

In-process, throw loudly instead of swallowing:

function mustParse(text, where) {
  try { return JSON.parse(text); }
  catch (e) { throw new SyntaxError(`${where}: ${e.message}`); }
}
import json, sys

def must_load(path: str) -> object:
    with open(path, encoding="utf-8") as fh:
        try:
            return json.load(fh)
        except json.JSONDecodeError as exc:
            sys.exit(f"{path}: line {exc.lineno} col {exc.colno}: {exc.msg}")  # sys.exit is a real alias

All three of those exit codes were checked on a valid and an invalid file. Silent fallbacks — try: parse() except: pass — turn a five-second fix into a post-mortem.

FAQ

Is there a way to sort keys or keep comments? Not in this tool. No key-sorting control exists, and comments aren't preserved — a commented file is not JSON and the validator will say so at the exact column.

Why does the validator disagree with my Node script? Node tolerates leading/trailing whitespace; strict parsers follow RFC 8259 byte-for-byte. Trust the stricter one. See patterns and gotchas.

Paste 50 MB and the tab dies — why? Everything is in-browser, one thread, and the parse holds the whole document plus the formatted copy in memory. Split it or stream it; see the NDJSON guide for the streaming route.

Developer Sponsor / Partner
Copied to clipboard!