Posted in Blog · Reading time ~7 min

JSON formatting: 2 spaces vs 4 spaces vs tabs

There is no official JSON formatting standard. In practice, 2-space indentation has won across every major ecosystem (npm, Python, Rust, Go's json.MarshalIndent, Java's Jackson default, VS Code, GitHub's display). 4-space and tabs survive in niche corners. This post covers why 2 won, the small handful of cases where 4 or tabs are still the right call, and the three other formatting decisions (key order, line breaks, trailing newlines) that matter when JSON ends up in git.

You can pick your indent live in the JSON Formatter via the gear icon — 2 spaces, 4 spaces, or tabs. The "Sort keys (deep)" option there is the one I'd recommend leaving on for files that live in version control. More on that below.

The spec says nothing

RFC 8259 does not specify formatting at all. {"a":1} and

{
  "a": 1
}

are equally valid and represent the same value. The spec only cares that the bytes parse. Anything you do with whitespace is for the humans reading the file.

Why 2 spaces won

Three reasons, none of them mysterious.

Nesting depth. Real-world JSON nests 4-6 levels deep without effort (request body → object → array → object → field). At 4 spaces that's 24 columns of leading whitespace before you see any content. At 2 spaces it's 12. The screen real-estate math wins.

Defaults. JSON.stringify(obj, null, 2) uses 2 spaces. json.dumps(obj, indent=2) in Python uses 2 spaces. Go's json.MarshalIndent(obj, "", " ") conventionally uses 2 spaces in every example you've ever read. npm init writes package.json with 2 spaces. When every example you copy-paste uses 2 spaces, your codebase ends up with 2 spaces whether you decided or not.

Diff-friendliness. At 2 spaces, more content fits within the 80-column "review-friendly" diff width, especially in side-by-side GitHub views.

When 4 spaces is actually right

One case: configuration files that humans edit by hand, with very flat structure and long string values. tsconfig.json in some projects, .vscode/settings.json, top-level tox.ini-ish stuff. At depth 1 or 2, 4 spaces reads more like prose and the wider indent helps tired eyes pick out the structure. PyCharm and IntelliJ's defaults for hand-edited JSON are 4 spaces and there's no particular reason to fight them.

Pick a convention per file type if you must. Don't pick a convention per file; that way lies madness.

When tabs is actually right

One case: JSON used as a data wire format that humans rarely look at but occasionally need to (gzipped server-to-server feeds, transport in a job queue). Tabs are one byte vs N for spaces, which is a real win on multi-GB files, and the indentation respects each viewer's tab-width preference.

The wider you set your editor's tab width when looking at deeply nested JSON, the more painful it gets, so tabs scale worse than 2 spaces past depth 4-ish. If your JSON is shallow and big, tabs are fine. If it's deep and humans read it, 2 spaces.

The compact one-line form

Don't forget the fourth option:

{"id":1,"name":"Alice","tags":["pro","early"]}

This is the right answer when JSON is on the wire and the consumer is a machine. JSON.stringify(obj) (with no indent) does this. Use it for:

  • HTTP response bodies (where gzip will recover most of the bytes anyway, but every \n is one round of compression context wasted).
  • JSONL / NDJSON record bodies — see the JSONL post — where the whole point is one record per line.
  • Embedded JSON inside another document (HTML data attributes, log fields). Pretty-printed JSON inside an HTML attribute is a maintenance hazard.

Key ordering

JSON object members are unordered by spec, but every formatter writes them out in some order. The two options are:

  • Insertion order. The order keys were added to the object. JavaScript objects preserve this since ES2015; JSON.stringify emits in that order. Most languages' default JSON serializers do likewise.
  • Sorted (alphabetical, deep). Diff-stable. {"name": "Alice", "id": 1} and {"id": 1, "name": "Alice"} produce the same bytes after sorting, so you don't get noisy diffs when a producer reorders fields.

For JSON in git — config files, fixtures, JSON Schemas — sort. For JSON on the wire — API responses, log records — leave it alone; ordering can be semantically useful (Stripe puts the most-relevant fields first; that's part of the UX of their API).

Concrete recipe: configure your pre-commit hook to run jq --sort-keys . file.json > file.json on any JSON file with a stable shape. The next time someone's editor decides to reorder their keys, the diff stays clean.

Line length

Long strings — embedded snippets, base64 blobs, long URLs — turn into 800-column lines if you don't think about them. No JSON formatter splits a string across lines (it can't; JSON strings can't contain literal newlines). Your options:

  • Accept the long lines. Most modern editors soft-wrap; GitHub renders fine.
  • Move the long values out of the JSON. Base64 blobs particularly. A schema with a file path or a hash, plus the blob in a sibling file, ages better in git.
  • Switch to YAML for files where multiline strings dominate. YAML's block scalar syntax (|) is one of the few things YAML genuinely does better than JSON.

Trailing newline

End the file with a newline. cat behaves sanely, git diff doesn't show the "no newline at end of file" marker, POSIX is happy. Every editor's "save" should add one. This is one of the few things on which I have no nuance.

Brace style

The two encountered options:

// "Egyptian" (what every formatter produces)
{
  "name": "Alice"
}

// "Allman" (what nobody produces, but I have seen it)
{
  "name": "Alice"
}

OK, the two are the same here because JSON has no statements after an opening brace — only members. Where the question shows up is in array-of-object formatting. Compact:

"orders": [
  { "id": "A-1", "amount": 49.99 },
  { "id": "A-2", "amount": 12.50 }
]

vs expanded:

"orders": [
  {
    "id": "A-1",
    "amount": 49.99
  },
  {
    "id": "A-2",
    "amount": 12.50
  }
]

The expanded form is what every standard indent=2 serializer produces. The compact form is more readable for short inline records but you can't get there from a built-in serializer; you'd have to post-process. I generally don't bother — the expanded form is consistent and diffs cleanly.

Recap

  • Format with 2 spaces. Match every ecosystem default and move on.
  • Sort keys deep on any JSON that lives in git.
  • Minify on the wire (no whitespace) for machine-to-machine traffic and JSONL records.
  • Trailing newline always.
  • If you have a strong reason to deviate, document it at the top of the file with a "$_format" comment-style key. Future-you will appreciate it.

The fastest way to apply this in practice: paste your JSON into the online JSON formatter and beautifier, pick 2-space indent with sort-keys deep, then run the result through the JSON validator to confirm it still parses. If you're committing JSON to git, the structural JSON diff will tell you whether a noisy textual diff is a real change or just a reformat.

Disagree about tabs? Yell at us — we'll add the counterargument as a footnote.