Javid
·8 min read

Pretty JSON: What Formatting Changes Besides Whitespace

SelfDevKit JSON Tools turning minified JSON into pretty JSON with indentation and a collapsible tree view

Pretty JSON is JSON with line breaks and indentation added so a human can read it. In theory that's all a pretty printer does: it adds whitespace, and the data stays exactly the same. In practice, most pretty printers parse your JSON into native values and then serialize them again, and that round trip can change things. Large IDs get rounded. Keys get reordered. Duplicate keys vanish. NaN becomes null, or turns into output that isn't valid JSON at all.

This post shows how to get pretty JSON in JavaScript, Python and jq, and then what each one does to the data along the way. Every output below comes from running the tools (Node 24, Python 3.12, jq 1.7.1) on the same input.

How to make pretty JSON in three common tools

To make pretty JSON, parse the text and serialize it again with an indent setting. Every mainstream language has this built in.

JavaScript uses the third argument of JSON.stringify:

const pretty = JSON.stringify(JSON.parse(raw), null, 2);   // 2 spaces
const tabbed = JSON.stringify(JSON.parse(raw), null, '\t'); // tabs

A number indent is capped at 10. Passing 20 still gives you 10 spaces.

Python comes with a command-line pretty printer, json.tool:

python3 -m json.tool data.json                      # 4 spaces by default
python3 -m json.tool --indent 2 --sort-keys data.json
python3 -m json.tool --tab --no-ensure-ascii data.json

The --no-ensure-ascii flag matters more than it looks. Without it, Python escapes every non-ASCII character, so "é" comes out as "\u00e9" and an emoji becomes a surrogate pair like "\ud83d\ude00". That's still valid JSON, but much harder to read, which defeats the point.

jq is the usual choice in a shell pipeline:

curl -s https://api.example.com/users/1 | jq .
jq --indent 4 . data.json
jq --tab -S . data.json     # tabs, sorted keys

The fidelity test: one input, three outputs

Pretty printing is lossless only if the parser can represent every value in the input exactly. Here is a single-line test document with eight common edge cases:

{"b":1,"10":"ten","a":2,"2":"two","id":9007199254740993,"price":1.10,"big":1e400,"neg":-0,"exp":1E2,"dup":1,"dup":2,"emoji":"café 😀"}

Here's what each tool printed:

Input Node JSON.stringify Python json.tool jq 1.7.1 .
Key order b, 10, a, 2 2, 10, b, a (reordered) preserved preserved
9007199254740993 9007199254740992 (wrong) exact exact
1.10 1.1 1.1 1.10
1e400 null Infinity (invalid JSON) 1E+400
-0 0 0 -0
1E2 100 100.0 1E+2
"dup":1,"dup":2 one key, value 2 one key, value 2 one key, value 2
"café 😀" café 😀 caf\u00e9 \ud83d\ude00 (escaped) café 😀

So none of the three gives byte-for-byte fidelity, and each fails differently. Some of these differences are cosmetic. Two of them can corrupt production data.

Big integers: the change that breaks things

JavaScript stores every JSON number as an IEEE 754 double, which represents integers exactly only up to 2^53 - 1 (9007199254740991). Anything larger is rounded to the nearest double without any warning. The ID 9007199254740993 came back as ...992, so it now points to a different record.

This isn't a hypothetical edge case. 64-bit IDs like Snowflake IDs and database BIGINT keys routinely exceed it. RFC 8259, section 6 says only integers in the range -(2^53)+1 to 2^53-1 are interoperable, because widely used implementations rely on IEEE 754 double precision.

jq 1.7 keeps the original literal as long as you don't do arithmetic on it. Once you do, it converts to a double too:

$ jq '.id, (.id + 0)' in.json
9007199254740993
9007199254740992

Modern JavaScript can preserve big integers if you ask. The reviver's third argument exposes the original source text, and JSON.rawJSON (Baseline 2025) writes it back out unchanged:

const data = JSON.parse(raw, (key, value, ctx) =>
  Number.isInteger(value) && !Number.isSafeInteger(value)
    ? BigInt(ctx.source)
    : value
);

const pretty = JSON.stringify(
  data,
  (key, value) => typeof value === 'bigint' ? JSON.rawJSON(value.toString()) : value,
  2
);
// "id": 9007199254740993 (exact)

Without the replacer, JSON.stringify throws Do not know how to serialize a BigInt. That's a better outcome than a silently wrong ID.

Key order: JavaScript moves numeric keys first

JavaScript objects list integer-like keys ("2", "10") in ascending numeric order before all other keys, whatever order they had in the source. Any formatter built on JSON.parse therefore prints "2" and "10" at the top. Python dicts and jq keep insertion order.

The JSON spec says objects are unordered, so this doesn't change the meaning. It does matter in two places:

  • Diffs. If you prettify both sides of a comparison with different tools, you get changes that don't exist. Use one tool for both sides, or sort keys on both sides (jq -S, --sort-keys). The JSON compare guide covers semantic diffing in more detail.
  • Signed payloads. Webhook signatures and JWS are computed over exact bytes. Verify against the raw body you received, never against a pretty-printed copy.

Sorting has its own detail. jq -S and Python's --sort-keys both sort by code point, so uppercase sorts before lowercase: B, Z, a, b, é. If you expected a, b, B, that's why the output looks wrong.

Duplicate keys disappear without an error

All three tools accepted {"dup":1,"dup":2} and kept only the last value. RFC 8259, section 4 says names "SHOULD be unique" and that behavior with duplicates is unpredictable; many implementations report only the last pair, as these three do. The trouble is that a pretty printer then hides a bug that was right there in the raw text, such as a config file that sets timeout twice.

To find duplicates in Python, use object_pairs_hook:

import json

def no_dupes(pairs):
    keys = [k for k, _ in pairs]
    dupes = {k for k in keys if keys.count(k) > 1}
    if dupes:
        raise ValueError(f"duplicate keys: {sorted(dupes)}")
    return dict(pairs)

json.load(open("config.json"), object_pairs_hook=no_dupes)
# ValueError: duplicate keys: ['dup']

NaN, Infinity and values JSON can't hold

JSON has no NaN or Infinity, so each tool falls back to something different. Python's encoder writes NaN and Infinity by default, and most other parsers reject that output. Pass allow_nan=False to json.dumps to get an error instead. JavaScript and jq both convert NaN to null.

JavaScript also drops some values without any notice. undefined and functions are removed from objects, and Date objects become ISO strings:

JSON.stringify({ u: undefined, f() {}, d: new Date(0), n: NaN })
// {"d":"1970-01-01T00:00:00.000Z","n":null}

That one only matters when you prettify a live object instead of JSON text. That happens all the time with console.log(JSON.stringify(obj, null, 2)), and it's why a field you know exists can be missing from the log.

Choosing a pretty printer for the job

The right pretty printer depends on whether you need to read the data or keep it exactly as it is.

Situation Use
Reading an API response or log line Any of them
Payload with 64-bit IDs or BIGINT keys jq (no arithmetic), or JS with ctx.source + JSON.rawJSON
Committing formatted JSON to git One tool with sorted keys, run in a pre-commit hook
Auditing a config for duplicate keys Python with object_pairs_hook
Non-ASCII text you need to read jq, JS, or Python with --no-ensure-ascii
Verifying a signature None; use the raw bytes

For deeply nested data, indentation alone isn't enough. A collapsible tree view helps you get around, and our JSON viewer guide covers that. If the formatter rejects your input outright, the JSON validator guide lists the syntax errors that usually cause it.

Pretty JSON in SelfDevKit

SelfDevKit's JSON Tools formats JSON locally on your machine. You get a collapsible tree view, 2, 4 or 6-space indentation, recursive key sorting, search with filtering, and collapse-by-arrays or collapse-by-objects. Nothing is uploaded, so you can paste a production response containing tokens or customer records without handing it to a third-party website. The online JSON formatter guide covers the privacy trade-offs of web formatters.

SelfDevKit JSON Tools showing pretty JSON in a collapsible tree view with indentation settings

To be clear about its limits: JSON Tools parses input with JavaScript's JSON.parse, so the behavior described above applies. Integers above 2^53 are rounded, numeric keys move to the top, and duplicate keys collapse to the last value. For everyday reading, debugging and sorting, that doesn't matter. When the payload carries 64-bit IDs that must survive a copy and paste, use jq . for that one task.

It also catches a common log-file annoyance: escaped JSON strings are detected and unescaped automatically (see how to unescape JSON). When you need the compact form back for a payload or a fixture, jq -c . or JSON.stringify(data) reverses the process, and the JSON minify guide explains when that's worth doing.

Download SelfDevKit to get pretty JSON, a tree view and 50+ other developer tools offline, without sending your data anywhere.

Related Articles

JSON Formatter Online: Format, Validate, and Debug JSON the Right Way
DEVELOPER TOOLS

JSON Formatter Online: Format, Validate, and Debug JSON the Right Way

Use a JSON formatter online to beautify and validate JSON data. Learn the best tools, privacy risks, and code examples.

Read →
JSON Validator: How to Find and Fix JSON Errors Fast
DEVELOPER TOOLS

JSON Validator: How to Find and Fix JSON Errors Fast

Use a JSON validator to find syntax errors fast. Fix common mistakes and validate programmatically in JS and Python.

Read →
JSON Minify: How to Compress JSON and Where It Actually Helps
DEVELOPER TOOLS

JSON Minify: How to Compress JSON and Where It Actually Helps

Learn how to JSON minify for production, cut file sizes by up to 40%, and avoid the privacy risks of pasting sensitive data into online tools.

Read →
How to Unescape JSON: A Practical Guide for Developers
DEVELOPER TOOLS

How to Unescape JSON: A Practical Guide for Developers

Learn how to unescape JSON strings in Python, JavaScript, and Go with code examples and debugging tips.

Read →