Skip to main content

jsondiffhtml

The fastest way to turn two JSON files into a shareable, reviewable HTML diff report.

PyPI Python versions License: MIT Zero dependencies

jsondiffhtml reads two JSON files, deep-diffs them (dicts, lists, primitives, and unordered lists of objects), and writes a single, self-contained HTML report you can email, commit, attach to a ticket, or host anywhere. No CDN calls, no external stylesheets, no runtime dependencies.

pip install jsondiffhtml
json-diff before.json after.json -o report.html

Why jsondiffhtml?

Most Python JSON-diff libraries hand you back a Python dict. That's useful if you're writing a program — useless if you're the reviewer on a pull request, the engineer debugging a broken deploy, or the ops person approving a config change. Those people want a diff they can read.

jsondiffhtml deepdiff xlwings/jsondiff
Output format HTML report (self-contained) Python dict Python dict
Grouping by JSON path ✅ ❌ ❌
Searchable UI with filter tabs ✅ ❌ ❌
Source line numbers per diff ✅ ❌ ❌
Per-item ignore & comment boxes ✅ ❌ ❌
Identifier-based list matching ✅ partial partial
Include/exclude path filters ✅ ✅ ✅
Themeable (color, title, logo) ✅ n/a n/a
Auto-splits very large reports ✅ n/a n/a
CLI included ✅ (json-diff) ❌ ✅ (jdiff)
Runtime dependencies 0 1 (orderly-set) 0
Python 3.9+ ✅ ✅ ✅

Use jsondiffhtml when you need a human-readable diff artifact. Use deepdiff or xlwings/jsondiff when you need a programmatic dict to drive application logic.


Quick start — CLI

# minimal
json-diff before.json after.json -o report.html

# themed, with a logo in the sidebar
json-diff before.json after.json -o report.html \
  --title "Release 2.4 config diff" \
  --primary-color "#16a34a" \
  --logo-url https://example.com/logo.svg

# filter to one subtree
json-diff before.json after.json -o report.html \
  --include-path "root['services']" \
  --exclude-path "root['services']['internal']"

Open report.html in a browser. Everything is inline — you can attach it to an email, host it on a static bucket, or drop it into a Slack thread.

Quick start — Python

from jsondiffhtml import JsonDiff, HtmlReporter, ReportConfig

diff = JsonDiff.from_files("before.json", "after.json")
print(f"{len(diff.objects_changed)} changed, "
      f"{len(diff.objects_added)} added, "
      f"{len(diff.objects_removed)} removed")

HtmlReporter(
    diff,
    ReportConfig(title="Release 2.4 config diff", primary_color="#16a34a"),
).write("report.html")

Each item in diff.objects_changed, diff.objects_added, diff.objects_removed is a frozen dataclass carrying the source path, destination path, old/new values, source-line numbers, and a human-readable label (see ListIdentifier below).

Matching unordered list items by identifier

When your JSON contains lists of objects whose order doesn't matter (users, resources, records), tell jsondiffhtml which key identifies each item so a reordered-but-unchanged list doesn't flood your report with false positives.

from jsondiffhtml import JsonDiff, HtmlReporter, ListIdentifier

diff = JsonDiff(
    before,
    after,
    identifiers_list=[
        ListIdentifier(
            parent_path="root['users']",
            id_key_name="id",
            get_identifier_value_func=lambda u: f"{u['first_name']} {u['last_name']}",
        ),
    ],
)
HtmlReporter(diff).write("users_diff.html")
  • parent_path is the path to the list with [<int>] indices stripped (root['users'], not root['users'][0]).
  • get_identifier_value_func is optional. If set, its return value becomes the readable label shown on each diff row — so reviewers see "Jane Doe" instead of root['users'][17].

Filtering

JsonDiff(
    a, b,
    included_paths=["root['config']"],              # keep only diffs whose path contains this
    excluded_paths=["root['config']['secret']"],    # drop diffs whose (index-stripped) path matches exactly
)

Large reports

When the total item count exceeds ReportConfig.split_threshold, write multiple files instead of one massive HTML:

HtmlReporter(diff, ReportConfig(split_threshold=50_000, max_items_per_file=30_000)) \
    .write_split("out_dir", "report")
# Produces: out_dir/report_changed_0.html, _added_0.html, _removed_0.html, ...

ReportConfig reference

Field Default Purpose
title "JSON Diff Report" Page title and sidebar heading
logo_url None Optional image shown above the title
primary_color "#2563eb" Any CSS color — drives the theme via CSS variables
group_results True True = group items by path; False = flat chronological list
show_line_numbers True Annotate paths with (line N)
include_annotations True Render per-item Ignore checkbox and comment textarea
split_threshold 50_000 When write_split() sees more items than this, splits per-category
max_items_per_file 30_000 Chunk size when splitting

Python API reference

from jsondiffhtml import (
    JsonDiff,                      # core diff engine
    ListIdentifier,                # identifier-based list matching
    ReportConfig,                  # report customization
    HtmlReporter,                  # HTML generation
    AddedObj, ChangedObj, RemovedObj,   # result dataclasses
    JsonPathLineNumberMapper,      # path → line number lookup (used internally; public for advanced use)
)

JsonDiff constructors:

  • JsonDiff(obj_a, obj_b, identifiers_list=None, included_paths=None, excluded_paths=None) — from already-parsed Python objects.
  • JsonDiff.from_files(path_a, path_b, **kwargs) — load JSON from disk.

HtmlReporter methods:

  • render() -> str — return the full HTML document as a string.
  • write(path) -> Path — write a single-file report.
  • write_split(dir, basename) -> list[Path] — auto-split on large reports.

CLI reference

json-diff [-h] [-o OUTPUT] [--title TITLE] [--primary-color COLOR]
          [--logo-url URL] [--flat] [--no-annotations] [--no-line-numbers]
          [--include-path PATH] [--exclude-path PATH] [--version]
          file_a file_b

Development

git clone https://github.com/OWNER/jsondiffhtml && cd jsondiffhtml
pip install -e ".[dev]"
pytest               # 32 tests
ruff check src/
mypy src/jsondiffhtml
python -m build      # wheel + sdist

Changelog

See CHANGELOG.md.

License

MIT — see LICENSE.

Metadata

Release files for jsondiffhtml 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for jsondiffhtml 0.1.0
File Size Uploaded
jsondiffhtml-0.1.0.tar.gz 27.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for jsondiffhtml 0.1.0
File Interpreter ABI Platform
jsondiffhtml-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 49.9 kB

Release files / jsondiffhtml-0.1.0.tar.gz

Download URL jsondiffhtml-0.1.0.tar.gz
Size 27.3 kB
Tags Source
SHA-256 checksum
How to use checksums
33b0c4e060d259b0c2299ecb9f7166c4b3299f1742436732d8ae261c403fbb3c
BLAKE2b-256 checksum
How to use checksums
2cbbebb35aecb560091f404e51d695f7417a17b77e0fd9b4d1a0c3f3a13a03d8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.11

Release files / jsondiffhtml-0.1.0-py3-none-any.whl

Download URL jsondiffhtml-0.1.0-py3-none-any.whl
Size 22.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a124e5b055788eb52cdb4c56b653d732b6da57b91e8a9d46297300ffefe99be0
BLAKE2b-256 checksum
How to use checksums
53789129140280617576f70f197fe8d8b3f2bbdc1fe502b011fe6688cb22f33f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.11

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page