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 — true does not test-equal 1 (booleans and numbers are distinct JSON types), but as of ADR-0017, 1 does test-equal 1.0: numbers compare by numeric value, not Python runtime type or spelling (a breaking change from earlier releases — see the CHANGELOG's migration notes)
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 ("", per RFC 6902 §4.1) is supported since ADR-0017 B-prime: add("", value) replaces the whole document, envelope preserved (through 0.6.x this raised InvalidAddPath).
move Supported — RFC 6902 §4.4, a remove composed with an add. Rejects moving a location into one of its own children. Moving a location to itself (any path, including root) is a validated exact-byte no-op, not a rejection, since ADR-0017 B-prime.
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, independent of earlier mutations in the same batch, so one deletion or insertion can't corrupt another edit's position. (Coordinate independence, not result commutativity — two adds at the same append position (/-) still land in queue order: add /- 1 then add /- 2 gives [1,2]; the reverse call order gives [2,1]. See algorithm.py's own docstring for why queue order is required behavior, not an accident, for equal-position insertions. move/copy share this same tie-break whenever they land at an already-shared destination gap — it's a rule by membership (any insertion-producing operation), not an add-only exception, M8-R20.)

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 — was originally computed by json-source-map, a small, elegant library by David Andersson, and this package depended on it as an ordinary PyPI dependency through v0.6.0. As of ADR-0017's semantic-domain work, that walker is vendored directly into src/json_source_edit/lexical_scan.py, with one correctness fix applied at the point it builds a value's path (a member literally named "a/b" used to collide with a nested .a.b path in the upstream library; the fork builds collision-free path tuples instead of raw path strings — see PROVENANCE.md for the full delta and the MIT license text). The credit is unchanged even though the dependency relationship is: the position-tracking logic this package builds everything else on top of is still David Andersson's design and implementation, and this package is now, as a direct consequence, dependency-free at runtime.

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 1,536-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) — plus two structurally-independent runtime oracles checked on every apply(validate=True) call, and a machine-verified, content-bound audit of every Move/Remove/root/cardinality claim in three source files' docstrings and comments against the code — editor.py, operations.py, lexical_oracle.py: 221 candidates, every one independently confirmed executable-or-false (ADR-0017 §M8; docs/adr/companions/0017-M8-R8-PROSE-PERIMETER-RECEIPTS.md). Claim-bearing prose in the rest of the source tree is checked by a lighter-weight disposition ledger (same document, "Companion files"/"Ledger" sections), not this same exhaustive standard. 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 1.0.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 json-source-edit 1.0.0
File Size Uploaded
json_source_edit-1.0.0.tar.gz 436.8 kB Details

Built distribution (wheel)

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

Total release size: 630.5 kB

Release files / json_source_edit-1.0.0.tar.gz

Download URL json_source_edit-1.0.0.tar.gz
Size 436.8 kB
Tags Source
SHA-256 checksum
How to use checksums
ec40b8bb43f0e634396028d6a555c3145cbd504a584c2abd6cca16ca8a425f84
BLAKE2b-256 checksum
How to use checksums
685c4161f2a41bb78ce593880c4318f024bf9b0d565d39f0d9716b0c1b7973ed
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 16, 2026.

Transparency log

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

Download URL json_source_edit-1.0.0-py3-none-any.whl
Size 193.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8a38a8002ca0dfaffc884e4d915e6988c4a808c22739e3fa51fea19b6e280475
BLAKE2b-256 checksum
How to use checksums
4538f0bff5bf4a7b668a510cdc580e1724dbc2c71efc55d517d5aafbf5d2e98c
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 16, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

0.6.0

2 release files

0.5.1

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