CLI tool
xlwings/jsondiff avatar
xlwings/jsondiff

jsondiff: a structural diff for JSON that returns JSON

Diff JSON and JSON-like structures in Python

749 stars91 forksPythonMIT

At a glance

What is it?
jsondiff compares two JSON-like structures in Python and returns the difference as a dictionary that is itself JSON shaped, with insert, update, and delete keys, three output syntaxes, path exclusion, special handling for sets, and a command line client called jdiff that reads and writes JSON or YAML.
Who is it for?
jsondiff fits a Python project that needs to compare two JSON documents and act on the difference, in configuration drift, in API responses, or in test assertions, and that wants the result to be machine readable rather than a printed text diff.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 46 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A diff whose output is data

jsondiff diffs JSON and JSON-like structures in Python, and the design decision that makes it different from a text diff is that the result is a dictionary.

The quickstart shows the shape.

python
>>> import jsondiff as jd
>>> from jsondiff import diff

>>> diff({'a': 1, 'b': 2}, {'b': 3, 'c': 4})
{'c': 4, 'b': 3, delete: ['a']}

A key was added, a key was changed, a key was removed, and the answer is a mapping with the change markers as keys. That means the difference can be stored, logged, serialised, and applied by other code, which is what a text diff cannot do.

The library is not limited to strict JSON types. Lists are compared positionally, and the result for a list difference is an insert or a delete carrying the index, so a list that grew at the end is an insert at the end, and a list that lost an element in the middle is a delete at that index.

python
>>> diff(['a', 'b', 'c'], ['a', 'b', 'c', 'd'])
{insert: [(3, 'd')]}

>>> diff(['a', 'b', 'c'], ['a', 'c'])
{delete: [1]}

The nested case works the same way, and the result is as small as you would hope, since only the changed leaf is reported.

python
>>> diff({'a': [0, {'b': 4}, 1]}, {'a': [0, {'b': 5}, 1]})
{'a': {1: {'b': 5}}}

The project is MIT licensed, the last push was on 2026-09-07, and the releases are 2.2.1 from 2024-08-29, 2.2.0 from 2024-07-20, and 2.1.2 from 2024-07-14.

Three syntaxes for the same difference

The diff function takes a syntax argument, and there are three choices, and they are worth understanding because they change the shape of the result rather than the content.

The default is the compact syntax, which is what the first example shows, where the changed values sit directly under their path and the removals are collected in a delete list. It is the shortest output and the one the library is tuned around.

The explicit syntax separates the three kinds of change into their own keys.

python
>>> diff({'a': 1, 'b': 2}, {'b': 3, 'c': 4}, syntax='explicit')
{insert: {'c': 4}, update: {'b': 3}, delete: ['a']}

The symmetric syntax goes the other way, and it is the one to reach for when you want to reconstruct both sides. Instead of a changed value being shown as the new one, both the old and the new value are kept.

python
>>> diff({'a': 1, 'b': 2}, {'b': 3, 'c': 4}, syntax='symmetric')
{insert: {'c': 4}, 'b': [2, 3], delete: {'a': 1}}

The naming of the three is the key to remembering them. Compact is the default and the terse one. Explicit marks the operation type at the top level. Symmetric keeps both sides of a change, so it is the one that supports a true patch.

The command line exposes the same choice, with the syntax flag and the compact, symmetric, and explicit values, and the default is compact there too, so a script using the library and a script using the command line produce the same thing by default.

The keyword in the project metadata lists diff, diffing, difference, patch, delta, dict, and LCS, which is a good summary of the vocabulary, including the mention of the longest common subsequence, which is the algorithm behind the list comparison.

Excluding paths, and the similarity question

Two features make the library usable on real data rather than on tidy examples.

The first is path exclusion. You can exclude some JSON paths from the diff, and the README is honest about the limitation, which is that it does not work if the value types are different.

python
>>> diff({'a': 1, 'b': {'b1': 20, 'b2': 21}, 'c': 3},  {'a': 1, 'b': {'b1': 22, 'b2': 23}, 'c': 30}, exclude_paths=['b.b1', 'c'])
{'b': {'b2': 23}}

That is the natural way to handle a document with a timestamp or a generated identifier in it, and the caveat is about the implementation rather than about the idea. If the value at an excluded path changed type, the path no longer matches the same thing, so the exclusion cannot apply.

The second is how list similarity is judged, and the example is the interesting one because the two lists are not simply different at one position.

python
>>> diff({'a': [0, {'b': 4}, 1]}, {'a': [0, {'c': 5}, 1]})
{'a': {insert: [(1, {'c': 5})], delete: [1]}}

The middle element changed from a dictionary with one key to a dictionary with a different key, and the diff reports it as a delete at one position and an insert at the same position rather than as a modification of the nested dictionary. That is the longest common subsequence at work, and it is the right behaviour for a diff, because reporting a delete and an insert is a statement about the two sequences that a consumer can act on, while reporting a modification of a nested object would be a guess about intent.

Sets get their own handling, which is not surprising, since a set has no order and therefore no position.

python
>>> diff({'a', 'b', 'c'}, {'a', 'c', 'd'})
{discard: set(['b']), add: set(['d'])}

The key collision problem, and the marshal option

There is a trap in the default output, and the README flags it explicitly with a note that the default keys in the result are objects, not strings.

The problem is that the marker keys are Python objects, which means they cannot collide with string keys in your data. Here is the case.

python
>>> d = diff({'a': 1, 'delete': 2}, {'b': 3, 'delete': 4})
>>> d
{'delete': 4, 'b': 3, delete: ['a']}
>>> d[jd.delete]
['a']
>>> d['delete']
4

The document on both sides has a key called delete, with the value 2 on one side and 4 on the other, and the diff contains both the changed value under the string key and the removal list under the object key. Reading the result by subscripting the string key gives you the value from the second document, and reading it by the marker object gives you the list of removed paths. The two are distinguished by type, and that is what stops them from overwriting each other.

If you want the result to be plain JSON, there is an alternative. The marshal option returns strings with a leading dollar sign instead of the objects.

python
>>> diff({'a': 1, 'delete': 2}, {'b': 3, 'delete': 4}, marshal=True)
{'delete': 4, 'b': 3, '$delete': ['a']}

That is the same result with the markers as strings, which means it serialises directly, at the cost of the type safety that made the default unambiguous.

There is a related pair of options for handling input and output as text rather than as objects, with load and dump, which parse a JSON string on the way in and serialise on the way out. The example in the README for that combination is written in the Python 2 print style rather than the function call style, which is a small inconsistency in the documentation rather than in the code, but it is worth knowing if you copy it literally.

The command line client, and the formats it handles

The library ships with a command line client called jdiff, and the usage line is short.

code
jdiff [-h] [-p] [-s {compact,symmetric,explicit}] [-i INDENT] [-f {json,yaml}] first second

Two positional arguments, the two files to compare. The patch flag produces a patch rather than a diff, the syntax flag chooses the rendering with compact as the default, the indent flag sets the number of spaces to indent, where none means a compact output with no indentation, and the format flag chooses the file format for both input and output, with JSON as the default.

The examples show the three combinations that matter.

bash
$ jdiff a.json b.json -i 2

$ jdiff a.json b.json -i 2 -s symmetric

$ jdiff a.yaml b.yaml -f yaml -s symmetric

The third one is why the format flag exists. The YAML support is not a second implementation, it is the same library with the parsing and serialising swapped, which is why the runtime dependency list is just one entry, PyYAML. Everything else in the library is about the data model, not about the wire format, and that separation is what lets the same comparison work on a JSON document, on a YAML document, and on a plain Python dictionary that came from somewhere else entirely.

The patch flag is the one to be careful with, because a patch is a different contract from a diff. A diff tells you what changed. A patch tells you how to get from the first document to the second, which means the ordering and the representation have to be consistent with what a patch consumer expects, and the symmetric syntax is the one that keeps both sides of every change, which is what a patch needs.

Packaging, the AI policy, and who maintains it

The repository is a small Python package with a conventional layout. There is the package directory, the tests, the documentation, the changelog, a manifest file, and both a pyproject file and a setup file, where the setup file is explicitly kept for legacy compatibility and does nothing but call setup.

The build is setuptools with a version derived from the source control tag, written out to a version file inside the package, and the dependencies and the optional dependencies are read from the requirements files rather than being listed in the metadata. The project requires Python 3.8 or newer, declares the MIT licence from a licence file, and names the author as a company, Zoomer Analytics, with the Python 3 classifier and the MIT classifier.

The development workflow is the standard two commands, installing the development requirements and then running the tests with pytest. The release check builds the package with the build module and runs the twine check over the distributions.

The contributing section has four steps, and the second one is the one that matters. Build and test your feature, and all new code, including bug fixes, must include unit tests. Then open a pull request, referencing the issue number if there is one, and collaborate with the maintainers as needed.

There is also an AI policy, and it is short and unusually reasonable. Please disclose the use of AI when opening your pull request. The use of AI does not disqualify your submission. And contributors are encouraged to have their agents practice brevity unless verbose detail is truly adding value.

That is a policy that treats the disclosure as the important part rather than the prohibition, which is a defensible position for a diff library whose whole purpose is to make machine comparison easy.

Editorial conclusion

jsondiff fits a Python project that needs to compare two JSON documents and act on the difference, in configuration drift, in API responses, or in test assertions, and that wants the result to be machine readable rather than a printed text diff. It does not fit a case where you need to know which of two orderings is correct, because the output records changes rather than proving equivalence, and it does not fit a project that needs a canonicalisation you can rely on across versions. Verify first that the syntax you choose matches how you intend to consume the result, since the same difference is rendered three different ways, and that the key naming does not collide with your own data, which is what the marshal option exists to solve.

Frequently asked questions

What does the jsondiff library return?

A dictionary that is itself JSON shaped, with insert, update, and delete markers as keys, so the difference can be stored, logged, serialised, and acted on by other code. Lists are compared positionally and report the index of the change, and sets are handled with dedicated add and discard markers.

What are the diff syntaxes in jsondiff?

Three. The compact syntax is the default and the tersest, the explicit syntax separates inserts, updates, and deletes into their own top level keys, and the symmetric syntax keeps both the old and the new value for a change, which is the one to use when you need to reconstruct either side.

Can I exclude parts of a document from a jsondiff comparison?

Yes, with the exclude_paths option, which takes a list of JSON paths such as a nested key and a top level key. The README notes the limitation that exclusion does not work if the value types at those paths are different, because the path then no longer refers to the same thing.

Why do jsondiff results use marker objects instead of strings?

So that the marker keys cannot collide with string keys in your own data. The README demonstrates the case where both documents contain a key called delete, and notes that with the default the result keys are objects rather than strings. You can get strings with a leading dollar sign instead by using the marshal option.

How do I use jsondiff from the command line?

With the jdiff client, which takes the two files to compare as positional arguments, plus flags for the syntax, the indent width, the file format for input and output which is JSON or YAML, and a patch option. The package is installed with pip install jsondiff.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. xlwings/jsondiff on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/xlwings-jsondiff.svg)](https://hysenlabs.com/projects/xlwings-jsondiff)

Was this useful?