JSON Formatter
Parse, re-indent, and validate. Input is held in memory; the document never traverses the wire.
1{2 "user": {3 "id": "8f3c",4 "name": "Ada Lovelace",5 "roles": [6 "admin",7 "editor"8 ],9 "meta": {10 "created": "2026-04-21",11 "active": true12 }13 }14}
Whitespace is infrastructure.
JSON's grammar permits any amount of insignificant whitespace between tokens, which means the same logical document can be serialized as a single 4 KB line or as a 200-line tree with two-space indentation. That flexibility sounds harmless until it collides with the tools developers actually use. Version control is the first place the seams show. A pretty-printed configuration file produces line-oriented diffs that Git can analyze, that GitHub can render with red and green gutters, and that reviewers can comment on at the level of an individual key. A minified blob of the same content produces a single-line diff that says "everything changed."
Three-way merges degenerate into conflicts that have to be resolved by hand, and the blame view becomes useless for archaeology. Teams that store JSON in version control essentially must adopt a pretty-print convention, and they must enforce it consistently or the diffs lie about what changed. Code review is the second pressure point. Reviewers reading a pull request that touches a JSON manifest need to see structure, not transport-optimized bytes. Indentation reveals nesting depth at a glance, and trailing commas — or the absence of them in strict JSON — become visible.
The third pressure point is incident response. Production logs almost always serialize structured events as compact, single-line JSON because each log line should be one record and because log shippers like Fluent Bit, Vector, and the AWS CloudWatch agent expect newline-delimited JSON. That choice is correct for ingestion but hostile to human inspection during an incident. An on-call engineer who pipes a 600-character log line through jq . is performing the format-on-view half of a broader pattern: minify for transport, beautify for inspection. Some teams formalize this as format-on-save in editors, combined with format-on-view in log tooling, and minify only at the network or storage boundary.
Indent, sort, and array layout.
The dominant pretty-print configuration knobs are indent width, key ordering, and array layout. Indent width is the most visible and the most bikeshed-prone. Two spaces is the JavaScript and Node.js default and is what JSON.stringify(obj, null, 2) emits. Four spaces is the Python convention inherited from PEP 8 and from json.dumps(obj, indent=4). A literal tab character is occasionally chosen because it lets each reader configure visual width independently, but tabs interact badly with JSON embedded in YAML or in Markdown code fences and most teams avoid them for portability. The choice rarely matters technically; what matters is that the choice is uniform across a repository so diffs reflect content changes rather than re-indentation.
Key ordering is the more consequential decision. JSON.stringify in Node.js preserves insertion order and offers no built-in sort option, which means two services emitting "the same" object can produce byte-different output if they constructed the object in different orders. Python's json.dumps(obj, indent=2, sort_keys=True) sorts keys lexicographically by default in many style guides, and Go's encoding/json package sorts map keys alphabetically when marshaling a map (though it preserves struct field declaration order). The convention of sorting keys in API responses exists precisely so that output is stable across language runtimes, across library versions, and across deployments. Stable output is what makes response caching, ETag computation, and snapshot testing reliable.
| Stack | Indent default | Sort keys | Array layout |
|---|---|---|---|
| JSON.stringify (Node.js) | none | no | vertical when indent > 0 |
| json.dumps (Python) | none | no | vertical when indent set |
| encoding/json MarshalIndent | specified | maps yes | vertical |
| jq . | 2 spaces | yes | vertical |
| prettier --parser json | 2 spaces | no | vertical with heuristics |
When bytes must match.
Pretty-printing solves human-readability problems. Canonical serialization solves a different problem: producing a byte-for-byte identical representation of a JSON document so that a cryptographic hash of those bytes is reproducible. RFC 8785, the JSON Canonicalization Scheme, was published in June 2020 and specifies exactly that. JCS sorts object members lexicographically by their UTF-16 code unit values, emits no insignificant whitespace, escapes strings in a specific way, and — critically — formats numbers using the ECMAScript Number.prototype.toString algorithm so that the same numeric value always produces the same textual representation regardless of the producing language.
JCS underpins a growing list of identity and security standards. The W3C Verifiable Credentials Data Integrity specification uses a canonicalization step before signing. COSE (CBOR Object Signing and Encryption, RFC 9052) has a JSON sibling profile that relies on canonical input, and JWS detached payload workflows that hash a JSON body need a canonical form to avoid signatures that break when an intermediary reformats whitespace. The distinction worth internalizing is that "pretty-printed with sorted keys" is not canonical. Canonical JSON has very specific rules about Unicode escape sequences, about which characters must be escaped versus represented literally, and especially about number formatting. Two implementations that both "sort keys and remove whitespace" will produce different bytes for 1.0, for 1e10, and for any number whose decimal representation requires careful rounding — and that mismatch will invalidate signatures.
A common mistake is to treat JSON.stringify(obj) after a manual key sort as equivalent to JCS output. It is not. JCS requires UTF-16 code unit ordering of keys (which differs from UTF-8 byte ordering for code points above U+FFFF), and it mandates the ECMAScript number algorithm. Use a vetted JCS implementation such as canonicalize on npm, the jcs Go module, or the Python rfc8785 package rather than rolling your own.
Shortest round-trip and why it matters.
Numbers are where JSON formatting becomes subtle. The JSON grammar allows integers, decimals, and scientific notation, and it places no upper bound on the magnitude or precision of a number literal. In practice, almost all parsers map JSON numbers to IEEE 754 double-precision floats, which provides about 15.95 decimal digits of precision. The textual representation chosen by a serializer matters because it determines whether a value round-trips exactly through serialize-deserialize-reserialize cycles.
JSON.stringify follows the ECMA-262 specification, which requires producing the shortest string that, when parsed back to a double, yields the original value. That algorithm — sometimes called "shortest round-trip" — is why JSON.stringify(0.1) returns "0.1" rather than the longer mathematically-precise representation, even though the latter is closer to the actual stored bit pattern. The same algorithm explains the famous 0.1 + 0.2 === 0.3 failure: the sum is the next representable double above 0.3, so JavaScript prints it as 0.30000000000000004, which is the shortest decimal that uniquely identifies that bit pattern.
Other languages choose differently. Python's json.dumps uses repr for floats, which since Python 3.1 also implements the shortest-round-trip algorithm, but earlier versions used a fixed 17-digit format. Go's encoding/json uses strconv.FormatFloat with the 'g' verb and -1 precision, which is also shortest-round-trip. Java's standard libraries vary by version. These differences are invisible until you try to compute a hash over the serialized bytes — and then a Python service and a Go service signing "the same" payload produce different signatures. RFC 8785 nails this down by requiring all conformant implementations to use the ECMAScript algorithm exactly.
Smaller payloads, measured savings.
Minification removes all insignificant whitespace and produces the smallest legal JSON encoding of a document. The conventional wisdom is that minification dramatically reduces bandwidth, and that wisdom is roughly a decade out of date. Modern HTTP responses are almost always served with Content-Encoding: gzip or br, and gzip's LZ77 sliding window compresses repeated whitespace runs to near-nothing. Empirical measurements on typical API payloads show that minifying before gzipping saves an additional 5 to 10 percent over pretty-printing before gzipping — meaningful but not the order-of-magnitude win that minification provides on uncompressed transport.
Minification still wins decisively in several specific scenarios. The first is pre-compression CPU. Gzip throughput on a typical x86 core is around 100 to 300 MB/s for level 6; minifying first reduces the input bytes the compressor has to scan, so a service that emits a million minified JSON responses per second pays measurably less CPU than one emitting pretty-printed JSON. The second scenario is non-gzipped channels: AWS Kinesis records, SQS message bodies, MQTT payloads on constrained IoT links, and certain inter-service RPC frames are transmitted without compression and charged or limited by raw byte count. The third is JSON literally embedded in JavaScript source that ships to browsers — every byte in the bundle is parsed and counted against script-size budgets. The fourth is push notifications: APNs limits payloads to 4 KB and FCM limits to 4 KB, both measured before any transport compression.
Before you spend engineering time minifying API responses, measure the wire size with and without minification under your actual Content-Encoding. If gzip is on and the difference is under 10 percent, the engineering effort is almost certainly better spent elsewhere. If you are over a tight notification or embedded-JSON budget, minification is non-negotiable.
Pretty before commit, minify at the boundary.
The most durable convention for repositories is: pretty-print before commit, minify only at the boundary. An .editorconfig file at the repository root specifies indent width and final newline policy for JSON files, and most modern editors honor it without additional configuration. A pre-commit hook — typically configured through the pre-commit framework or as a Husky hook in JavaScript projects — runs a formatter such as prettier --write so that nothing reaches the index in an inconsistent state. Polyglot repositories benefit most: a Python service, a Go service, and a TypeScript frontend in the same monorepo all emit JSON fixtures formatted identically, which means cross-service test fixtures diff cleanly when shared.
The command-line toolbox for ad-hoc work is small and stable. jq . pretty-prints with two-space indentation and sorted keys; jq -c . minifies to a single line per input document, which is the canonical form for newline-delimited JSON streams. python -m json.tool is available wherever Python 3 is installed and accepts --sort-keys and --indent. prettier is the de facto formatter for projects that already use it for JavaScript. Older tools like jsonpp still appear on macOS systems but have largely been superseded.
For documentation, JSON embedded in Markdown fenced blocks should be pretty-printed and kept short; a minified blob in a code fence is hostile to readers and to anyone copying the example into a request. Treating documentation JSON with the same formatting rigor as committed source files is the small discipline that distinguishes maintained projects from drifting ones.