Skip to main content

json-source-edit

A Python package for surgical, byte-preserving JSON editing. Change only what you intend to change — everything else in the file survives byte-for-byte, so your diff shows your edit, not a full-file rewrite.

The problem

The obvious way to edit a JSON file is parse → modify → serialize:

data = json.loads(text)
data["steps"][1]["line"] = 433
text = json.dumps(data, indent=2)

This is correct, and it destroys your diff. Real evidence, from the project this package was extracted out of: three simple changes to one file — two field updates and one deletion — were expected to produce a ~10-line diff. The parse-modify-serialize round-trip produced a 500+ line diff instead.

That's because json.dumps doesn't know or care what your original formatting was — indentation, key order, quote style, all of it gets re-decided from scratch, every time, whether or not you touched it. Multiply that by a codebase-sized JSON file and code review becomes impossible: every line looks changed, and the actual edit is buried.

json-source-edit fixes this at the source: it never re-serializes the document. It computes exact byte positions for each change against the original text and splices replacement bytes into the gaps — the same operation a film or tape splicer performs, cut and rejoin at an exact point, everything else on the reel undisturbed.

Install

pip install json-source-edit

(Or clone the repo and pip install -e . for local development.)

Usage

The package's whole reason to exist is editing files without disturbing their formatting, so that workflow comes first:

from json_source_edit import JSONEditor

editor = JSONEditor.from_file("config.json")
editor.replace("/items/0/id", 99)
editor.save()   # writes back to config.json; config.json.backup keeps the original

A no-argument save() writes to the path the editor was loaded from (as of 0.5.1 — earlier releases silently wrote nothing here; see docs/adr/0014-*.md) and raises NoSaveDestinationError if there is no destination at all (e.g. a from_string editor), so "saved" can never again be reported without bytes on disk.

Or purely in memory:

editor = JSONEditor.from_string("""{
  "title": "Rust Basics",
  "steps": [
    {"file": "src/main.rs", "line": 10},
    {"file": "src/lib.rs", "line": 20}
  ]
}""")
editor.replace("/steps/0/line", 433)
editor.remove("/steps/1")
result = editor.apply(validate=True)
print(editor.preview_diff())
--- original
+++ modified
@@ -1,7 +1,6 @@
 {
   "title": "Rust Basics",
   "steps": [
-    {"file": "src/main.rs", "line": 10},
-    {"file": "src/lib.rs", "line": 20}
+    {"file": "src/main.rs", "line": 433}
   ]
 }

validate=True catches mistakes before they reach you. It reconstructs the expected result independently — replaying your edits against a copy of the parsed document — and diffs that against what the surgical edit actually produced. If they disagree, it raises SemanticValidationError instead of silently returning something subtly wrong.

apply(), commit(), and save() all default to validate=True — pass validate=False to skip both checks and get the raw compiled result regardless, e.g. if you're doing your own validation or need the ~10-15% extra time this costs off the critical path (apply()'s default changed from False in 0.5.0, after a batch that emptied a container and added into it was found to silently return invalid JSON on every release from v0.1.0 through v0.4.0 — see docs/adr/0010-*.md).

Three more methods help you inspect a pending batch before committing to it: get_value, get_modifications, and preview_diff.

Scope

JSON Patch operation Status
replace Supported — any path, any depth
remove Supported — array elements and object properties, any depth, except the document root itself (structurally undefined: the root has no parent container to apply a comma/whitespace rule against)
test Supported — not a text edit, an assertion. Evaluated against the document as it stood before this batch's own operations (not a naive sequential reading of RFC 6902 — see the docstring on operations.Test), with type-strict comparison (1 does not test-equal 1.0 or true)
add Supported — new array index (any position, or - to append) or object key. An existing object key is replaced instead, per RFC 6902 — an existing array index is always an insert, never a replace (see docs/adr/0002-*.md). The document root is out of scope, same posture as remove.
move Supported — RFC 6902 §4.4, a remove composed with an add. Rejects moving a location into one of its own children, and moving a location to itself.
copy Supported — RFC 6902 §4.5, an add using a value resolved (and deep-copied) from elsewhere in the document.

Every JSON Patch operation is implemented. Multiple replace/remove/ test/add/move/copy calls batch correctly against the same original document — every path resolves in original-document coordinates regardless of what else is in the batch, so edit order doesn't matter and one deletion or insertion can't corrupt another edit's position.

Benchmark (v0)

Reproduce with python benchmarks/throughput.py — a standalone script, no test framework required.

Methodology: replaces 5% of a flat array's elements at each size, median of 5 runs, reporting throughput against the original document size (the more relevant number for "can this handle my file" than edit count alone).

Single-machine numbers, not a controlled benchmark environment — expect real variance run to run and machine to machine; rerun locally before relying on any of this for capacity planning.

Size Elements Document Edits Median time Throughput
small 200 0.01MB 10 0.65ms 15,353 edits/sec, 13.7 MB/sec
medium 2,000 0.09MB 100 6.25ms 15,990 edits/sec, 14.9 MB/sec
large 20,000 0.97MB 1000 87.22ms 11,465 edits/sec, 11.1 MB/sec

(Measured on macOS/arm64, Python 3.12.4, 2026-08-01 — see the script's own output for full per-run samples; the "large" row in particular showed bimodal timing on this machine, a real observation, not smoothed over.)

Provenance

This package was designed and hardened inside codetour-cli, across four independent review gates, before being extracted here with no known remaining incompleteness. See PROVENANCE.md for the commit-by-commit history and docs/adr/0001-extraction-from-codetour-cli.md for the extraction decisions themselves (import strategy, the dependency verdict, the supported-Python-versions floor).

Credits

The position-mapping this package edits against — turning a JSON document into exact byte offsets for every value and key — is done by json-source-map, a small, dependency-free library by David Andersson. It's the one piece of this package that isn't ours: everything in src/json_source_edit/source_map.py is a thin seam around it, and everything else in this package builds on top of what it computes. Elegant, narrowly-scoped libraries like this are what make a package like this one feasible to write at all.

The playground (interactive TUI)

An interactive semantics playground ships as an optional extra — experiment with every operation against a live document, with inline path autosuggest, input history, and a .jse-playground.log.jsonl session log written where you launch it:

pip install "json-source-edit[playground]"
jse-playground

Good first minutes: new [1, 2, 3], then move /0 /1, then undo. Sequential mode (the default) applies each operation immediately; mode batch switches to the library's pristine-coordinate batch contract for studying how large batches compose.

Status

Published: pypi.org/project/json-source-edit. pip install json-source-edit. A 300+-test suite with full JSON Patch operation coverage, including cold-artifact regression tests — added after line-coverage numbers were caught certifying a no-op (a covered branch is not a working feature; docs/adr/0014-*.md). See CHANGELOG.md for release history. Design decisions beyond the original extraction live in docs/adr/ (0002: add's insertion doctrine, including the move/copy compositions; 0003: test's pristine-batch semantics).

License

MIT.

Metadata

Release files for json-source-edit 0.5.1

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

Source distribution (sdist)

Source distribution for json-source-edit 0.5.1
File Size Uploaded
json_source_edit-0.5.1.tar.gz 86.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for json-source-edit 0.5.1
File Interpreter ABI Platform
json_source_edit-0.5.1-py3-none-any.whl Python 3 none any Details

Total release size: 136.3 kB

Release files / json_source_edit-0.5.1.tar.gz

Download URL json_source_edit-0.5.1.tar.gz
Size 86.2 kB
Tags Source
SHA-256 checksum
How to use checksums
76f5985028cfb3e116d3e2a7c91143bc781fd8f64564dd5a0a0aea75a455e94a
BLAKE2b-256 checksum
How to use checksums
efd88eb560a38ad74f92e4e4a3b1586255bb6851f8571ef7ce610fd1b94ff0bd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 6, 2026.

Transparency log

Release files / json_source_edit-0.5.1-py3-none-any.whl

Download URL json_source_edit-0.5.1-py3-none-any.whl
Size 50.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2fd4531614500b67f50bc065f3bf3d8ff9d143a7089412ac95722db02a4af5ee
BLAKE2b-256 checksum
How to use checksums
b13044a8d6c60d9dcf5977b3a3b065c5e88ee8d613763b5d35a7ca590b3e7e7e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 6, 2026.

Transparency log

Release history Release notifications | RSS feed

1.0.0

2 release files

0.6.0

2 release files

This release

0.5.1 This release

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

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