Skip to main content

Diff two JSON files and produce a modern, self-contained, themeable HTML report. No runtime dependencies.

Project description

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

jsondiffhtml-0.1.0.tar.gz (27.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

jsondiffhtml-0.1.0-py3-none-any.whl (22.6 kB view details)

Uploaded Python 3

File details

Details for the file jsondiffhtml-0.1.0.tar.gz.

File metadata

  • Download URL: jsondiffhtml-0.1.0.tar.gz
  • Upload date:
  • Size: 27.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.11

File hashes

Hashes for jsondiffhtml-0.1.0.tar.gz
Algorithm Hash digest
SHA256 33b0c4e060d259b0c2299ecb9f7166c4b3299f1742436732d8ae261c403fbb3c
MD5 2b37c142f3b06e6865753e5452b8da22
BLAKE2b-256 2cbbebb35aecb560091f404e51d695f7417a17b77e0fd9b4d1a0c3f3a13a03d8

See more details on using hashes here.

File details

Details for the file jsondiffhtml-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: jsondiffhtml-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 22.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.11

File hashes

Hashes for jsondiffhtml-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a124e5b055788eb52cdb4c56b653d732b6da57b91e8a9d46297300ffefe99be0
MD5 479e72ae73fe1a753e4a91dd9de9b6b1
BLAKE2b-256 53789129140280617576f70f197fe8d8b3f2bbdc1fe502b011fe6688cb22f33f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page