How to compare two JSON files

Why a plain text diff of JSON is misleading, and how to see only the changes that matter: values, added keys and removed keys.

You have two versions of a JSON file: an API response before and after a deploy, two config files, or a translation file from two branches. You want to know what changed. Pasting both into a text diff seems obvious, but it usually shows far more “changes” than there really are.

Why a plain diff of JSON misleads

JSON has two kinds of differences that don’t change the data at all:

  • Key order. {"a":1,"b":2} and {"b":2,"a":1} are the same object. Serializers, databases and languages reorder keys freely, so two equal objects can look completely different line by line.
  • Formatting. Minified JSON is one long line; pretty-printed JSON spreads the same data over hundreds. Indentation (2 spaces, 4 spaces, tabs) and spacing differ between tools.

A line diff treats both as changes. Compare a minified response with a pretty-printed one and every line differs, which hides the one value you were looking for.

The fix: normalize first, then diff

Make both sides look the same wherever the data is the same, then compare:

  1. Pretty-print both with the same indentation, so each key and value sits on its own line.
  2. Sort keys alphabetically, recursively, in every object. Equal objects now have identical lines.
  3. Keep array order. Arrays are ordered in JSON: [1,2] and [2,1] really are different, so they must not be sorted.
  4. Diff the result line by line, with changed values marked word by word.

The JSON diff does all four with one click on “Format JSON & sort keys”, in your browser:

Open the JSON diff

Doing it on the command line

With jq installed, -S sorts keys and the output is pretty-printed, so this gives the same normalized comparison:

diff <(jq -S . old.json) <(jq -S . new.json)

In Python, the same idea:

import json

def normalize(path):
    with open(path) as f:
        return json.dumps(json.load(f), indent=2, sort_keys=True).splitlines()

import difflib
print("\n".join(difflib.unified_diff(normalize("old.json"), normalize("new.json"), lineterm="")))

Reading the result

  • A changed value shows as a modified line, with the old and new value highlighted inside it.
  • An added key is a green line on the right only; a removed key is a red line on the left only.
  • A moved array item shows as removed in one place and added in another, because arrays are ordered.
  • Type changes are real changes: "42" (a string) is not 42 (a number).

Common problems

One file isn’t valid JSON

Trailing commas, comments and single quotes are allowed in JavaScript but not in JSON, and they stop any JSON-aware comparison. The validator shows the exact line and column:

Open the JSON validator

Big numbers change

IDs like 12345678901234567890 are larger than JavaScript numbers can hold exactly, so many tools quietly round them, and the rounding then shows up as a change, or worse, hides one. toolspace keeps numbers exactly as written.

Duplicate keys

{"id":1,"id":2} is technically allowed, but parsers keep only the last value, so a duplicate can hide data. The formatter and validator warn about them.

Private data

API responses often hold tokens or customer data. Online tools that upload your JSON to a server put that data at risk. Every toolspace tool runs in your browser, and the page is blocked from sending data anywhere.