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)
| File | Size | Uploaded | |
|---|---|---|---|
| json_source_edit-1.0.0.tar.gz | 436.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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