JSON Diff & Compare

Document A — original
Document B — changed
Added: 0 Removed: 0 Changed: 0 Type changed: 0

How to Compare Two JSON Files

Paste the original document into panel A and the changed one into panel B. The comparison runs automatically a moment after you stop typing, and every difference is listed below with the exact path to the value that moved. Nothing is uploaded — parsing and comparison happen in JavaScript in this tab, which is what makes it safe to paste a production API response or a Kubernetes manifest containing internal hostnames.

The output is a list of paths, not a list of lines. $.image.tag tells you precisely which value changed regardless of how either document was indented, in what order its keys were written, or whether one side arrived minified. That is the whole point of comparing JSON structurally instead of textually.

Why a Text Diff Is the Wrong Tool for JSON

Running diff or a side-by-side text comparison on JSON produces two failure modes, and both waste real time.

False positives. Key order is not meaningful in JSON, but it is preserved, so anything that regenerates a document — a serialiser, an export, a Terraform state refresh, a lockfile update — can emit the same data with keys in a different order. A text diff reports every one of those lines as changed. Reformatting from two-space to four-space indentation, or minifying, changes every line in the file while changing nothing at all about the data.

False negatives. Minified JSON is one enormous line. A text diff tells you that line 1 differs from line 1 and shows you 40 kilobytes of characters. The change is in there somewhere.

A structural diff parses both sides first, so formatting and key order stop existing before the comparison starts. Load the built-in example: documents A and B declare service and replicas in opposite orders, and that reordering is correctly reported as no difference at all.

The Four Kinds of Change

Every difference falls into exactly one of these categories, colour-coded in the results.

BadgeMeaningExample
ADDEDThe path exists in B but not in A$.env.RETRIES appears in the new config
REMOVEDThe path exists in A but not in B$.env.TIMEOUT was deleted
CHANGEDBoth sides hold a primitive of the same type, with different values"1.4.2""1.5.0"
TYPEBoth sides have the path, but the JSON types differ3"3"

Separating TYPE from CHANGED is deliberate, because the two mean very different things to whatever consumes the document. A version string moving from 1.4.2 to 1.5.0 is a normal edit. A replica count moving from the number 3 to the string "3" is usually a bug in whatever wrote the file — an environment variable interpolated without a cast, or a YAML round trip that quoted a scalar. It will pass a schema check that only tests presence, and fail at runtime. The example loaded by the button above contains exactly this, so you can see how it is reported.

The Three Options, and When Each Is Right

Ignore array order. Off by default, because array order is data — ["read","write"] and ["write","read"] may well be the same permission set, but the steps in a pipeline definition are certainly not interchangeable. Turn it on when the array is conceptually a set: tags, allowed origins, feature flags, security-group rules. With it on, elements are matched as a multiset, so a moved element is silent while a genuinely duplicated one is still reported.

Treat null as missing. Off by default. Many serialisers disagree about whether an empty field should be omitted or written as null: Jackson omits by default, encoding/json in Go writes null for a nil pointer, and Python's json.dumps writes null for None. Comparing output from two different stacks produces a wall of added-and-removed noise that means nothing. This option collapses it.

Ignore keys. A comma-separated list of key names, matched at any depth. This is the option that turns the tool from interesting into useful: real documents carry fields that change on every write and never matter to a comparison — updatedAt, lastModified, etag, resourceVersion, generation, requestId. Excluding them is usually the difference between a four-line diff you can read and a two-hundred-line one you cannot.

Three Comparisons That Report Nothing

These are not bugs in the tool. They are consequences of parsing JSON into JavaScript values, and they are worth knowing because they are the cases where a diff can genuinely mislead you.

1. 1.0 and 1 are the same value. JSON has a single number type and no integer/float distinction. Both sides parse to the number 1, so no difference is reported even though the two files are textually different. The same applies to 1e3 and 1000, and to 1.10 and 1.1. If the trailing zero matters to you — a price, a version segment, a fixed-precision measurement — it needed to be a string in the first place.
2. Very large integers compare equal when they are not. JavaScript parses JSON numbers into 64-bit floats, which represent integers exactly only up to 253−1. Two different 20-digit snowflake IDs or Postgres bigint values can round to the same float and report no difference. This affects every JSON tool in a browser, including this one. IDs that must survive comparison should be transmitted as strings — which is exactly why so many APIs quote them.
3. Duplicate keys collapse before the comparison happens. {"env":"staging","env":"prod"} parses to a single key with the value "prod", because the JSON specification permits parsers to accept repeats and JavaScript's takes the last one. A duplicate in one file and not the other therefore produces a confusing diff, or none. Our JSON formatter and validator makes the collapse visible: format the file and a key you expected twice appears once.

Where JSON Comparison Comes Up

Reviewing an infrastructure change. A Terraform plan, a CloudFormation template, or a Kubernetes manifest rendered before and after a change is the canonical use. Ignore resourceVersion, generation, creationTimestamp, and uid, and what remains is the change you are actually approving.

Debugging an API that changed under you. Capture a response today and a response from before the deploy, and compare. Structural diffing finds the field that was quietly renamed or retyped in seconds, where reading two formatted responses side by side rarely does. If the responses are wrapped in a signed token, our JWT decoder will extract the payload first.

Verifying a migration or a round trip. Convert a document to another format and back, then diff the result against the original. That is the only honest test that a conversion is lossless — and it is how the round-trip guarantees on our JSON to CSV converter and XML to JSON converter were checked. Config written as YAML can be normalised through the YAML to JSON converter first so both sides are directly comparable.

Writing a snapshot test. When a snapshot assertion fails, the framework usually prints a text diff of two pretty-printed blobs. Pasting both sides here, with volatile keys excluded, tells you in one line whether the change is real.

Comparing config across environments? Managed platforms such as DigitalOcean and Render expose the full rendered configuration of every service as JSON, so a staging-versus-production drift check is a copy, a paste, and a compare. These are affiliate links — they cost you nothing and help keep these tools free.

Frequently Asked Questions

Is my JSON uploaded anywhere?
No. Both documents are parsed and compared in JavaScript in your browser. Nothing is transmitted, stored, or logged, so production payloads and infrastructure manifests are safe to paste. You can confirm it in your browser's network tab: comparing produces no outbound requests.
Does key order count as a difference?
Never. Both documents are parsed before anything is compared, so key order, indentation, and minification are gone by the time the diff runs. Only the data is compared.
What does a path like $.ports[2] mean?
It is the location of the value in the document, JSONPath-style: $ is the root, dots step into object keys, and brackets index into arrays. A key that is not a plain identifier — one containing a dot, a space, or a dash — is shown bracket-quoted, as in $["content-type"], so the path stays unambiguous.
Why is a change inside an array reported as one added and one removed?
With array-order matching on (the default), arrays are compared position by position, so a value edited in place is reported as a change at that index. With "ignore array order" enabled there are no positions to compare, so an edited element is reported as the old value removed and the new one added, both at [*].
Can it show me a merged or patched result?
No, by design. Producing a merged document means choosing a winner for every conflict, and a tool that guesses silently is worse than one that shows you the conflicts. The report tells you exactly which paths disagree; the merge is yours to make.
Is there a size limit?
No hard limit — you are bounded by your browser's memory. Documents of a few megabytes compare without trouble. A diff with thousands of differences will render slowly, which is usually a sign that an ignore-key rule would make the result readable.