fastjsonpatch 0.1.2__tar.gz → 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/.github/workflows/CI.yml +19 -1
  2. fastjsonpatch-0.1.4/BENCHMARKS.md +57 -0
  3. fastjsonpatch-0.1.4/CHANGELOG.md +33 -0
  4. {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/Cargo.lock +1 -1
  5. {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/Cargo.toml +1 -1
  6. {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/MAINTAINING.md +23 -7
  7. fastjsonpatch-0.1.4/PKG-INFO +112 -0
  8. fastjsonpatch-0.1.4/README.md +84 -0
  9. {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/pyproject.toml +1 -1
  10. {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/python/fastjsonpatch/__init__.py +1 -2
  11. {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/src/lib.rs +5 -0
  12. {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/tests/test_parity.py +54 -0
  13. fastjsonpatch-0.1.2/CHANGELOG.md +0 -17
  14. fastjsonpatch-0.1.2/PKG-INFO +0 -206
  15. fastjsonpatch-0.1.2/README.md +0 -178
  16. {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/.gitignore +0 -0
  17. {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/LICENSE +0 -0
  18. {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/THIRD_PARTY_LICENSES.md +0 -0
  19. {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/bench/compare.py +0 -0
  20. {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/scripts/test_linux_matrix.sh +0 -0
  21. {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/scripts/third_party_licenses.py +0 -0
  22. {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/tests/rfc6902/LICENSE-APACHE-2.0 +0 -0
  23. {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/tests/rfc6902/README.md +0 -0
  24. {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/tests/rfc6902/spec_tests.json +0 -0
  25. {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/tests/rfc6902/test_rfc6902.py +0 -0
  26. {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/tests/rfc6902/tests.json +0 -0
  27. {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/tests/upstream/test_upstream.py +0 -0
  28. {fastjsonpatch-0.1.2 → fastjsonpatch-0.1.4}/tests/upstream/tests.js +0 -0
@@ -44,7 +44,10 @@ jobs:
44
44
  run: |
45
45
  python -m pip install --upgrade pip maturin
46
46
  maturin build --release --out dist
47
- python -m pip install --only-binary fastjsonpatch --find-links dist "fastjsonpatch[test]"
47
+ # Install the wheel just built by its path: with --find-links, pip prefers a
48
+ # PyPI wheel of the same version and platform tag over the local one.
49
+ wheel=$(ls dist/*.whl)
50
+ python -m pip install "${wheel}[test]"
48
51
  - name: Tests (parity with jsonpatch, upstream suite, RFC 6902 suite)
49
52
  shell: bash
50
53
  run: |
@@ -127,6 +130,21 @@ jobs:
127
130
  name: sdist
128
131
  path: dist
129
132
 
133
+ # One check for branch protection to require: fails unless every test, wheel and
134
+ # sdist job succeeded.
135
+ ci-ok:
136
+ name: ci-ok
137
+ needs: [test, wheels, sdist]
138
+ if: always()
139
+ runs-on: ubuntu-latest
140
+ steps:
141
+ - name: All required jobs succeeded
142
+ run: |
143
+ echo 'test=${{ needs.test.result }} wheels=${{ needs.wheels.result }} sdist=${{ needs.sdist.result }}'
144
+ test '${{ needs.test.result }}' = success
145
+ test '${{ needs.wheels.result }}' = success
146
+ test '${{ needs.sdist.result }}' = success
147
+
130
148
  publish:
131
149
  name: publish to ${{ startsWith(github.ref, 'refs/tags/v') && 'PyPI' || 'TestPyPI' }}
132
150
  needs: [test, wheels, sdist]
@@ -0,0 +1,57 @@
1
+ # fastjsonpatch benchmarks
2
+
3
+ ## Results
4
+
5
+ Measured on Linux x86_64, Python 3.13 (times are the best of several runs).
6
+
7
+ Real-world data:
8
+
9
+ | scenario | size | jsonpatch | fastjsonpatch | speedup |
10
+ |---|---|---|---|---|
11
+ | Kubernetes OpenAPI v1.33 (3.7 MB) → v1.34 (3.8 MB): make_patch | 283 ops | 136.4 ms | 9.6 ms | **14x** |
12
+ | Kubernetes OpenAPI v1.33 → v1.34: apply | 283 ops | 78.0 ms | 11.4 ms | **7x** |
13
+ | 3-op patch on the v1.34 spec | 3.8 MB | 71.1 ms | 10.1 ms | **7x** |
14
+ | 3-op patch on the v1.34 spec, `in_place=True` | 3.8 MB | 21.2 µs | 4.7 µs | **4x** |
15
+ | PyPI metadata snapshots (8k records): make_patch | 2,487 ops | 255.2 ms | 12.1 ms | **21x** |
16
+ | PyPI metadata snapshots: apply | 2,487 ops | 91.7 ms | 14.0 ms | **7x** |
17
+ | REST resource, apply_patch per request | ×1000 | 47.4 ms | 7.5 ms | **6x** |
18
+ | REST resource, make_patch per request | ×1000 | 74.4 ms | 5.6 ms | **13x** |
19
+
20
+ Synthetic records shaped like `{"id", "name", "score", "tags": [..], "meta": {..}}`:
21
+
22
+ | workload | ops | make_patch: jsonpatch | fastjsonpatch | speedup | apply: jsonpatch | fastjsonpatch | speedup |
23
+ |---|---|---|---|---|---|---|---|
24
+ | 10k records, 100 field edits | 100 | 292.61 ms | 5.26 ms | **56x** | 89.38 ms | 7.90 ms | **11x** |
25
+ | 10k records, insert 1 at front | 49911 | 669.77 ms | 42.78 ms | **16x** | 535.21 ms | 72.35 ms | **7x** |
26
+ | 10k records, delete 50 | 44900 | 636.53 ms | 40.46 ms | **16x** | 563.20 ms | 63.45 ms | **9x** |
27
+ | 10k records, move 20 | 26305 | 434.93 ms | 24.38 ms | **18x** | 325.64 ms | 41.03 ms | **8x** |
28
+ | 2k ints, insert 1 at front | 1 | 17.04 ms | 1.91 ms | **9x** | 581 µs | 21 µs | **27x** |
29
+ | small config (200 keys), 1 edit | 1 | 1.98 ms | 35 µs | **57x** | 464 µs | 32 µs | **15x** |
30
+
31
+ Python 3.14 made `copy.deepcopy` about 1.5x faster, so on 3.14 jsonpatch's `apply` on
32
+ a large document is faster too; on the "10k records, 100 field edits" workload the
33
+ `apply` speedup drops from about 11x to about 6x. The `make_patch` speedup stays about
34
+ the same. Run `python bench/compare.py` (from the source distribution) to measure on
35
+ your own machine.
36
+
37
+ ## Where the time goes
38
+
39
+ **make_patch.** jsonpatch walks both documents in Python and compares every scalar
40
+ with `json.dumps(a) == json.dumps(b)`. Profiling the "10k records" workload with
41
+ cProfile puts about 40% of the time in `json.dumps` and about a third in `isinstance`
42
+ checks against abstract base classes (`MutableMapping`, `MutableSequence`).
43
+ fastjsonpatch walks the documents in Rust over the CPython objects directly (no
44
+ conversion to a Rust data structure), compares values natively with the same rules as the `json.dumps` comparison (`1`, `1.0` and `True`
45
+ are all different, `-0.0 != 0.0`, NaN equals NaN), keeps the current path on a stack,
46
+ and takes a single lockstep pass when two dicts have the same keys in the same order.
47
+ It is a port of jsonpatch's `DiffBuilder`, so it produces the same operations in the
48
+ same order, including detected moves.
49
+
50
+ **apply.** jsonpatch first runs `copy.deepcopy` on the document (so the original is
51
+ not modified), then builds a `JsonPointer` and an operation object for each operation.
52
+ For a small patch on a large document the copy is nearly all of the time.
53
+ fastjsonpatch copies each dict and list with one C call and only recurses into values
54
+ that are themselves containers; it keeps `deepcopy`'s semantics (shared and cyclic
55
+ references are copied once) but skips the bookkeeping for containers referenced only
56
+ once. It then resolves the paths and runs the operations without creating
57
+ `JsonPointer` or operation objects.
@@ -0,0 +1,33 @@
1
+ # Changelog
2
+
3
+ ## 0.1.4 (2026-10-12)
4
+
5
+ - `import fastjsonpatch` takes about 11 ms instead of about 53 ms: `__version__` now
6
+ comes from the Rust module instead of `importlib.metadata`, which is no longer
7
+ imported. (jsonpatch 1.34 takes about 11 ms.)
8
+ - CI: the macOS and Windows test jobs installed the wheel from PyPI instead of the
9
+ one built in the job whenever both had the same version; they now install the
10
+ built wheel by path. No change to the package.
11
+
12
+ ## 0.1.3 (2026-10-12)
13
+
14
+ - Fix: `replace` with a path ending in `-` that names a dict key (`{"-": 1}`,
15
+ path `/-`) was applied; jsonpatch 1.34 raises `InvalidJsonPatch` for it, and
16
+ fastjsonpatch now does too.
17
+ - Shorter README; full benchmark tables moved to BENCHMARKS.md.
18
+
19
+ ## 0.1.2 (2026-10-11)
20
+
21
+ Documentation only: the README now states only measured facts (including exactly
22
+ which platforms CI tests), and its link to MAINTAINING.md works on PyPI. No code
23
+ changes. (0.1.1 was tagged but not published.)
24
+
25
+ ## 0.1.0 (2026-10-11)
26
+
27
+ First release. Behaves like jsonpatch 1.34 with `make_patch`, `apply`/`apply_patch`
28
+ and patch validation in Rust:
29
+
30
+ - `make_patch` 9-57x faster, `apply` 4-27x faster (see BENCHMARKS.md).
31
+ - Standalone: depends only on `jsonpointer`.
32
+ - Wheels for Linux (glibc and musl, x86_64 and aarch64), macOS (arm64, x86_64) and
33
+ Windows (x64); one abi3 wheel per platform covers Python 3.12+.
@@ -23,7 +23,7 @@ checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
23
23
 
24
24
  [[package]]
25
25
  name = "fastjsonpatch"
26
- version = "0.1.2"
26
+ version = "0.1.4"
27
27
  dependencies = [
28
28
  "ahash",
29
29
  "pyo3",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "fastjsonpatch"
3
- version = "0.1.2"
3
+ version = "0.1.4"
4
4
  edition = "2024"
5
5
  description = "Rust core of the fastjsonpatch Python package"
6
6
  license = "BSD-3-Clause"
@@ -19,7 +19,7 @@ jsonpatch 1.34. A behaviour difference is a bug, unless it is listed in the READ
19
19
  ```sh
20
20
  maturin develop --release
21
21
  pip install "jsonpatch==1.34" pytest
22
- pytest tests # ~1570 tests, a few seconds
22
+ pytest tests # ~1580 tests, a few seconds
23
23
  python bench/compare.py # speed vs jsonpatch
24
24
  ```
25
25
 
@@ -33,6 +33,19 @@ python bench/compare.py # speed vs jsonpatch
33
33
  CI runs the tests and the speed comparison on Linux x86_64/arm64, macOS arm64/x86_64
34
34
  and Windows for Python 3.12, 3.13 and 3.14; the run's summary page has the table.
35
35
 
36
+ ## Making changes
37
+
38
+ Every change goes through a pull request; nothing is pushed to `main` directly.
39
+
40
+ 1. Branch from `main` (`fix/...`, `feat/...`, `chore/...`), commit, push the branch.
41
+ 2. Open a pull request. CI runs on it; the `ci-ok` check passes only when every
42
+ test, wheel and sdist job passed.
43
+ 3. Review the diff on the pull request page, then merge (squash or merge commit).
44
+
45
+ `main` has a branch rule that requires a pull request and the `ci-ok` check. It does
46
+ not require an approving review: GitHub does not let the author approve their own
47
+ pull request, and the maintainer pushes with their own account.
48
+
36
49
  ## Versions
37
50
 
38
51
  [Semantic versioning](https://semver.org): `MAJOR.MINOR.PATCH`.
@@ -46,19 +59,22 @@ and Windows for Python 3.12, 3.13 and 3.14; the run's summary page has the table
46
59
  While on 0.x the API may still change between minor versions. Go to 1.0 once the
47
60
  package has had real-world use without behaviour bugs.
48
61
 
49
- The version lives only in `pyproject.toml` (`[project] version`);
50
- `fastjsonpatch.__version__` reads it from the installed package. `Cargo.toml` is not
51
- published and can stay at 0.1.0.
62
+ The published version comes from `pyproject.toml` (`[project] version`);
63
+ `fastjsonpatch.__version__` reads it from the installed package. Keep `Cargo.toml`
64
+ and `Cargo.lock` at the same version (they ship in the sdist).
52
65
 
53
66
  ## Releasing
54
67
 
55
- 1. Update the version in `pyproject.toml` and `CHANGELOG.md`; merge to `main` with green CI.
68
+ 1. In a pull request, update the version in `pyproject.toml`, `Cargo.toml` (and
69
+ `Cargo.lock`) and `CHANGELOG.md`; merge it with a green `ci-ok`.
56
70
  2. Optional dry run: Actions → CI → "Run workflow" with target `testpypi`, then
57
71
  `pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ fastjsonpatch`.
58
72
  A version number can be uploaded to TestPyPI only once as well, so use a dev
59
73
  version (e.g. `0.1.1.dev1`) for dry runs.
60
- 3. `git tag v0.1.1 && git push origin v0.1.1`. CI checks that the tag equals the
61
- version, builds every wheel and the sdist, and publishes to PyPI.
74
+ 3. On the merged `main` commit: `git tag v0.1.1 && git push origin v0.1.1`. CI checks
75
+ that the tag equals the version and builds every wheel and the sdist; the publish
76
+ job then waits for approval in the `pypi` environment (Actions → the run →
77
+ Review deployments) and uploads to PyPI.
62
78
 
63
79
  A version uploaded to PyPI can be yanked (hidden from new installs) but its number can
64
80
  never be reused. Fix mistakes with a new PATCH release.
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastjsonpatch
3
+ Version: 0.1.4
4
+ Classifier: Development Status :: 4 - Beta
5
+ Classifier: Intended Audience :: Developers
6
+ Classifier: Operating System :: OS Independent
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: Programming Language :: Rust
10
+ Classifier: Programming Language :: Python :: Implementation :: CPython
11
+ Classifier: Topic :: Software Development :: Libraries
12
+ Requires-Dist: jsonpointer>=3.0,<4
13
+ Requires-Dist: pytest ; extra == 'test'
14
+ Requires-Dist: jsonpatch==1.34 ; extra == 'test'
15
+ Provides-Extra: test
16
+ License-File: LICENSE
17
+ License-File: THIRD_PARTY_LICENSES.md
18
+ Summary: Drop-in faster jsonpatch (RFC 6902): make_patch and apply in Rust
19
+ Keywords: json,patch,jsonpatch,rfc6902,diff,rust
20
+ Author-email: bitsbybyun@gmail.com
21
+ License-Expression: BSD-3-Clause
22
+ Requires-Python: >=3.12
23
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
24
+ Project-URL: Changelog, https://github.com/amzntiger/fastjsonpatch/blob/main/CHANGELOG.md
25
+ Project-URL: Issues, https://github.com/amzntiger/fastjsonpatch/issues
26
+ Project-URL: Repository, https://github.com/amzntiger/fastjsonpatch
27
+
28
+ # fastjsonpatch
29
+
30
+ A drop-in replacement for [jsonpatch](https://github.com/stefankoegl/python-json-patch)
31
+ ([RFC 6902](https://www.rfc-editor.org/rfc/rfc6902)) with `make_patch` and `apply`
32
+ written in Rust. It returns the same patches and documents as jsonpatch 1.34 and raises
33
+ exceptions with the same types and messages.
34
+
35
+ ```sh
36
+ pip install fastjsonpatch
37
+ ```
38
+
39
+ ```python
40
+ import fastjsonpatch as jsonpatch # the only line you change
41
+
42
+ src = {"name": "web", "replicas": 3, "ports": [80, 443]}
43
+ dst = {"name": "web", "replicas": 5, "ports": [80, 443, 8080]}
44
+
45
+ patch = jsonpatch.make_patch(src, dst)
46
+ patch.patch
47
+ # [{'op': 'replace', 'path': '/replicas', 'value': 5},
48
+ # {'op': 'add', 'path': '/ports/2', 'value': 8080}]
49
+
50
+ patch.apply(src) == dst # True; src is not modified
51
+ jsonpatch.apply_patch(src, [{"op": "remove", "path": "/ports/0"}])
52
+ # {'name': 'web', 'replicas': 3, 'ports': [443]}
53
+ ```
54
+
55
+ Requires CPython 3.12+. Wheels are published for Linux (x86_64, aarch64; glibc and
56
+ musl), macOS (arm64, x86_64) and Windows (x64). The only dependency is `jsonpointer`.
57
+
58
+ ## Performance
59
+
60
+ Linux x86_64, Python 3.13, best of several runs:
61
+
62
+ | workload | jsonpatch | fastjsonpatch | speedup |
63
+ |---|---|---|---|
64
+ | `make_patch`, Kubernetes OpenAPI v1.33 → v1.34 (3.7 → 3.8 MB, 283 ops) | 136.4 ms | 9.6 ms | **14x** |
65
+ | `apply`, the same 283 ops | 78.0 ms | 11.4 ms | **7x** |
66
+ | `make_patch`, 10k records with 100 field edits | 292.61 ms | 5.26 ms | **56x** |
67
+ | `apply`, the same 100 ops | 89.38 ms | 7.90 ms | **11x** |
68
+ | `make_patch`, small REST resource ×1000 | 74.4 ms | 5.6 ms | **13x** |
69
+ | `apply_patch`, small REST resource ×1000 | 47.4 ms | 7.5 ms | **6x** |
70
+
71
+ Across all measured workloads, `make_patch` is 9-57x faster and `apply` 4-27x (4x is a
72
+ 3-operation patch applied `in_place` to a 3.8 MB document, where no copy is made). On
73
+ Python 3.14, whose `copy.deepcopy` is faster, the `apply` speedup is smaller (about 6x
74
+ instead of 11x on the 10k-record workload). Full tables and an explanation of where
75
+ the time goes are in
76
+ [BENCHMARKS.md](https://github.com/amzntiger/fastjsonpatch/blob/main/BENCHMARKS.md);
77
+ `python bench/compare.py` (in the source distribution) measures on your machine.
78
+
79
+ ## Compatibility
80
+
81
+ fastjsonpatch is jsonpatch 1.34's module with `JsonPatch.__init__` (validation),
82
+ `JsonPatch.from_diff` (`make_patch`) and `JsonPatch.apply` (`apply_patch`) routed to
83
+ Rust; every other public name is jsonpatch's own code. Inputs the Rust code does not
84
+ handle exactly fall back to jsonpatch's original code before anything is modified:
85
+ non-JSON values, dict or list subclasses, nesting deeper than 1000, a custom `dumps=`
86
+ or `json_dumper`, a custom `pointer_cls`, `operations` or `_get_operation` (for
87
+ `apply`), and operations that would fail.
88
+
89
+ Known differences:
90
+
91
+ - The exception classes are fastjsonpatch's own, so
92
+ `except fastjsonpatch.JsonPatchConflict` does not catch an exception raised by the `jsonpatch` package, and vice versa.
93
+ - Values in a document that are not dicts, lists or JSON scalars are copied with
94
+ `copy.deepcopy` one by one, so aliasing between such values is not preserved.
95
+ - `__version__` is fastjsonpatch's version; `__jsonpatch_version__` is `"1.34"`.
96
+ jsonpatch's `__author__` and `__website__` are not defined.
97
+
98
+ ## Testing
99
+
100
+ About 1,580 tests: randomized comparisons against jsonpatch 1.34 (patches, documents
101
+ and errors), jsonpatch's own test suite and doctests, and the
102
+ [json-patch-tests](https://github.com/json-patch/json-patch-tests) RFC 6902 suite.
103
+ CI runs them with Python 3.12-3.14 on Linux (x86_64, aarch64; glibc), macOS (arm64,
104
+ x86_64) and Windows (x64); the musl wheels are built but not tested in CI.
105
+
106
+ ## License
107
+
108
+ BSD-3-Clause. The Python module and the diff algorithm come from jsonpatch
109
+ (Copyright (c) 2011 Stefan Kögl, BSD). Licenses of the Rust crates compiled into the
110
+ wheels are in `THIRD_PARTY_LICENSES.md`. Development and release notes:
111
+ [MAINTAINING.md](https://github.com/amzntiger/fastjsonpatch/blob/main/MAINTAINING.md).
112
+
@@ -0,0 +1,84 @@
1
+ # fastjsonpatch
2
+
3
+ A drop-in replacement for [jsonpatch](https://github.com/stefankoegl/python-json-patch)
4
+ ([RFC 6902](https://www.rfc-editor.org/rfc/rfc6902)) with `make_patch` and `apply`
5
+ written in Rust. It returns the same patches and documents as jsonpatch 1.34 and raises
6
+ exceptions with the same types and messages.
7
+
8
+ ```sh
9
+ pip install fastjsonpatch
10
+ ```
11
+
12
+ ```python
13
+ import fastjsonpatch as jsonpatch # the only line you change
14
+
15
+ src = {"name": "web", "replicas": 3, "ports": [80, 443]}
16
+ dst = {"name": "web", "replicas": 5, "ports": [80, 443, 8080]}
17
+
18
+ patch = jsonpatch.make_patch(src, dst)
19
+ patch.patch
20
+ # [{'op': 'replace', 'path': '/replicas', 'value': 5},
21
+ # {'op': 'add', 'path': '/ports/2', 'value': 8080}]
22
+
23
+ patch.apply(src) == dst # True; src is not modified
24
+ jsonpatch.apply_patch(src, [{"op": "remove", "path": "/ports/0"}])
25
+ # {'name': 'web', 'replicas': 3, 'ports': [443]}
26
+ ```
27
+
28
+ Requires CPython 3.12+. Wheels are published for Linux (x86_64, aarch64; glibc and
29
+ musl), macOS (arm64, x86_64) and Windows (x64). The only dependency is `jsonpointer`.
30
+
31
+ ## Performance
32
+
33
+ Linux x86_64, Python 3.13, best of several runs:
34
+
35
+ | workload | jsonpatch | fastjsonpatch | speedup |
36
+ |---|---|---|---|
37
+ | `make_patch`, Kubernetes OpenAPI v1.33 → v1.34 (3.7 → 3.8 MB, 283 ops) | 136.4 ms | 9.6 ms | **14x** |
38
+ | `apply`, the same 283 ops | 78.0 ms | 11.4 ms | **7x** |
39
+ | `make_patch`, 10k records with 100 field edits | 292.61 ms | 5.26 ms | **56x** |
40
+ | `apply`, the same 100 ops | 89.38 ms | 7.90 ms | **11x** |
41
+ | `make_patch`, small REST resource ×1000 | 74.4 ms | 5.6 ms | **13x** |
42
+ | `apply_patch`, small REST resource ×1000 | 47.4 ms | 7.5 ms | **6x** |
43
+
44
+ Across all measured workloads, `make_patch` is 9-57x faster and `apply` 4-27x (4x is a
45
+ 3-operation patch applied `in_place` to a 3.8 MB document, where no copy is made). On
46
+ Python 3.14, whose `copy.deepcopy` is faster, the `apply` speedup is smaller (about 6x
47
+ instead of 11x on the 10k-record workload). Full tables and an explanation of where
48
+ the time goes are in
49
+ [BENCHMARKS.md](https://github.com/amzntiger/fastjsonpatch/blob/main/BENCHMARKS.md);
50
+ `python bench/compare.py` (in the source distribution) measures on your machine.
51
+
52
+ ## Compatibility
53
+
54
+ fastjsonpatch is jsonpatch 1.34's module with `JsonPatch.__init__` (validation),
55
+ `JsonPatch.from_diff` (`make_patch`) and `JsonPatch.apply` (`apply_patch`) routed to
56
+ Rust; every other public name is jsonpatch's own code. Inputs the Rust code does not
57
+ handle exactly fall back to jsonpatch's original code before anything is modified:
58
+ non-JSON values, dict or list subclasses, nesting deeper than 1000, a custom `dumps=`
59
+ or `json_dumper`, a custom `pointer_cls`, `operations` or `_get_operation` (for
60
+ `apply`), and operations that would fail.
61
+
62
+ Known differences:
63
+
64
+ - The exception classes are fastjsonpatch's own, so
65
+ `except fastjsonpatch.JsonPatchConflict` does not catch an exception raised by the `jsonpatch` package, and vice versa.
66
+ - Values in a document that are not dicts, lists or JSON scalars are copied with
67
+ `copy.deepcopy` one by one, so aliasing between such values is not preserved.
68
+ - `__version__` is fastjsonpatch's version; `__jsonpatch_version__` is `"1.34"`.
69
+ jsonpatch's `__author__` and `__website__` are not defined.
70
+
71
+ ## Testing
72
+
73
+ About 1,580 tests: randomized comparisons against jsonpatch 1.34 (patches, documents
74
+ and errors), jsonpatch's own test suite and doctests, and the
75
+ [json-patch-tests](https://github.com/json-patch/json-patch-tests) RFC 6902 suite.
76
+ CI runs them with Python 3.12-3.14 on Linux (x86_64, aarch64; glibc), macOS (arm64,
77
+ x86_64) and Windows (x64); the musl wheels are built but not tested in CI.
78
+
79
+ ## License
80
+
81
+ BSD-3-Clause. The Python module and the diff algorithm come from jsonpatch
82
+ (Copyright (c) 2011 Stefan Kögl, BSD). Licenses of the Rust crates compiled into the
83
+ wheels are in `THIRD_PARTY_LICENSES.md`. Development and release notes:
84
+ [MAINTAINING.md](https://github.com/amzntiger/fastjsonpatch/blob/main/MAINTAINING.md).
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "fastjsonpatch"
7
- version = "0.1.2"
7
+ version = "0.1.4"
8
8
  description = "Drop-in faster jsonpatch (RFC 6902): make_patch and apply in Rust"
9
9
  readme = "README.md"
10
10
  license = "BSD-3-Clause"
@@ -50,7 +50,6 @@ Custom pointer classes, custom ``operations`` and subclasses that override
50
50
  import collections
51
51
  import copy
52
52
  import functools
53
- import importlib.metadata
54
53
  import json
55
54
  from collections.abc import MutableMapping, MutableSequence, Sequence
56
55
  from types import MappingProxyType
@@ -64,7 +63,7 @@ _ST_ADD = 0
64
63
  _ST_REMOVE = 1
65
64
 
66
65
 
67
- __version__ = importlib.metadata.version('fastjsonpatch')
66
+ __version__ = _core.__version__
68
67
  __license__ = 'BSD-3-Clause'
69
68
  # Version of jsonpatch whose behaviour this module reproduces.
70
69
  __jsonpatch_version__ = '1.34'
@@ -835,6 +835,9 @@ impl<'py> Applier<'_, 'py> {
835
835
  if parts.is_empty() {
836
836
  return Ok(Some(deepcopy(&v)?));
837
837
  }
838
+ if parts.last().is_some_and(|p| p == "-") {
839
+ return Ok(None); // jsonpatch rejects "-" here even for a dict key
840
+ }
838
841
  let Some(s) = slot(obj, parts)? else { return Ok(None) };
839
842
  if slot_value(&s)?.is_none() {
840
843
  return Ok(None);
@@ -962,5 +965,7 @@ fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> {
962
965
  m.add_function(wrap_pyfunction!(apply, m)?)?;
963
966
  m.add_function(wrap_pyfunction!(validate, m)?)?;
964
967
  m.add_function(wrap_pyfunction!(py_deepcopy, m)?)?;
968
+ // The package version, so `fastjsonpatch.__version__` needs no importlib.metadata.
969
+ m.add("__version__", env!("CARGO_PKG_VERSION"))?;
965
970
  Ok(())
966
971
  }
@@ -163,6 +163,25 @@ def test_invalid_patches(patch):
163
163
  assert outcome(lambda: fastjsonpatch.JsonPatch(patch)) == outcome(lambda: jsonpatch.JsonPatch(patch))
164
164
 
165
165
 
166
+ @pytest.mark.parametrize("doc, patch", [
167
+ # jsonpatch rejects a final "-" in replace even when it names a dict key
168
+ ({"-": 1}, [{"op": "replace", "path": "/-", "value": 2}]),
169
+ ({"a": {"-": 1}}, [{"op": "replace", "path": "/a/-", "value": 2}]),
170
+ ([1], [{"op": "replace", "path": "/-", "value": 2}]),
171
+ ({"-": 1}, [{"op": "remove", "path": "/-"}]),
172
+ ({"-": 1}, [{"op": "add", "path": "/-", "value": 2}]),
173
+ ({"-": 1}, [{"op": "test", "path": "/-", "value": 1}]),
174
+ ({"-": 1}, [{"op": "move", "from": "/-", "path": "/b"}]),
175
+ ({"-": 1}, [{"op": "copy", "from": "/-", "path": "/b"}]),
176
+ ])
177
+ def test_dash_key(doc, patch):
178
+ for in_place in (False, True):
179
+ a, b = copy.deepcopy(doc), copy.deepcopy(doc)
180
+ assert (outcome(lambda: fastjsonpatch.apply_patch(a, patch, in_place=in_place))
181
+ == outcome(lambda: jsonpatch.apply_patch(b, patch, in_place=in_place)))
182
+ assert repr(a) == repr(b)
183
+
184
+
166
185
  def test_exceptions_have_the_same_hierarchy():
167
186
  with pytest.raises(fastjsonpatch.JsonPatchConflict):
168
187
  fastjsonpatch.apply_patch({}, [{"op": "remove", "path": "/a"}])
@@ -188,6 +207,41 @@ def test_standalone_does_not_import_jsonpatch():
188
207
  assert out.stdout.strip() == "False"
189
208
 
190
209
 
210
+ def test_version_matches_the_installed_package():
211
+ # __version__ comes from Cargo.toml; the wheel's version from pyproject.toml.
212
+ import importlib.metadata
213
+ assert fastjsonpatch.__version__ == importlib.metadata.version("fastjsonpatch")
214
+
215
+
216
+ _SPY_IMPORTLIB_METADATA = """
217
+ import sys, traceback
218
+ preloaded = "importlib.metadata" in sys.modules
219
+ for name in [m for m in sys.modules if m == "importlib.metadata" or m.startswith("importlib.metadata.")]:
220
+ del sys.modules[name]
221
+ importers = []
222
+
223
+ class Spy:
224
+ def find_spec(self, name, path=None, target=None):
225
+ if name == "importlib.metadata":
226
+ importers.append("".join(traceback.format_stack(limit=15)))
227
+
228
+ sys.meta_path.insert(0, Spy())
229
+ import fastjsonpatch
230
+ print("preloaded:", preloaded)
231
+ print(importers[0] if importers else "")
232
+ """
233
+
234
+
235
+ def test_import_does_not_load_importlib_metadata():
236
+ # importlib.metadata alone used to add ~30 ms to `import fastjsonpatch`. Drop it
237
+ # from sys.modules first, in case the interpreter loaded it at startup, and
238
+ # record who imports it again.
239
+ out = subprocess.run([sys.executable, "-I", "-c", _SPY_IMPORTLIB_METADATA],
240
+ capture_output=True, text=True, check=True)
241
+ preloaded, _, importer = out.stdout.partition("\n")
242
+ assert not importer.strip(), f"{preloaded}; importlib.metadata imported by:\n{importer}"
243
+
244
+
191
245
  def test_aliasing_preserved_like_deepcopy():
192
246
  shared = [1, 2]
193
247
  doc = {"a": shared, "b": shared}
@@ -1,17 +0,0 @@
1
- # Changelog
2
-
3
- ## 0.1.2 (2026-10-11)
4
-
5
- Documentation only: the README now states only measured facts (including exactly
6
- which platforms CI tests), and its link to MAINTAINING.md works on PyPI. No code
7
- changes. (0.1.1 was tagged but not published.)
8
-
9
- ## 0.1.0 (2026-10-11)
10
-
11
- First release. Behaves like jsonpatch 1.34 with `make_patch`, `apply`/`apply_patch`
12
- and patch validation in Rust:
13
-
14
- - `make_patch` 9-57x faster, `apply` 6-27x faster (see README).
15
- - Standalone: depends only on `jsonpointer`.
16
- - Wheels for Linux (glibc and musl, x86_64 and aarch64), macOS (arm64, x86_64) and
17
- Windows (x64); one abi3 wheel per platform covers Python 3.12+.
@@ -1,206 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: fastjsonpatch
3
- Version: 0.1.2
4
- Classifier: Development Status :: 4 - Beta
5
- Classifier: Intended Audience :: Developers
6
- Classifier: Operating System :: OS Independent
7
- Classifier: Programming Language :: Python :: 3
8
- Classifier: Programming Language :: Python :: 3 :: Only
9
- Classifier: Programming Language :: Rust
10
- Classifier: Programming Language :: Python :: Implementation :: CPython
11
- Classifier: Topic :: Software Development :: Libraries
12
- Requires-Dist: jsonpointer>=3.0,<4
13
- Requires-Dist: pytest ; extra == 'test'
14
- Requires-Dist: jsonpatch==1.34 ; extra == 'test'
15
- Provides-Extra: test
16
- License-File: LICENSE
17
- License-File: THIRD_PARTY_LICENSES.md
18
- Summary: Drop-in faster jsonpatch (RFC 6902): make_patch and apply in Rust
19
- Keywords: json,patch,jsonpatch,rfc6902,diff,rust
20
- Author-email: bitsbybyun@gmail.com
21
- License-Expression: BSD-3-Clause
22
- Requires-Python: >=3.12
23
- Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
24
- Project-URL: Changelog, https://github.com/amzntiger/fastjsonpatch/blob/main/CHANGELOG.md
25
- Project-URL: Issues, https://github.com/amzntiger/fastjsonpatch/issues
26
- Project-URL: Repository, https://github.com/amzntiger/fastjsonpatch
27
-
28
- # fastjsonpatch
29
-
30
- A drop-in, faster [jsonpatch](https://github.com/stefankoegl/python-json-patch)
31
- ([RFC 6902](https://www.rfc-editor.org/rfc/rfc6902) JSON Patch) for Python.
32
- `make_patch` and `apply` run in Rust and return the **same patches and the same
33
- documents, and raise exceptions with the same types and messages,** as jsonpatch 1.34.
34
- In the benchmarks below, making a patch is 9-57x faster and applying one 6-27x faster.
35
-
36
- ```sh
37
- pip install fastjsonpatch
38
- ```
39
-
40
- ```python
41
- import fastjsonpatch as jsonpatch # the only line you change
42
-
43
- src = {"name": "web", "replicas": 3, "ports": [80, 443]}
44
- dst = {"name": "web", "replicas": 5, "ports": [80, 443, 8080]}
45
-
46
- patch = jsonpatch.make_patch(src, dst)
47
- patch.patch
48
- # [{'op': 'replace', 'path': '/replicas', 'value': 5},
49
- # {'op': 'add', 'path': '/ports/2', 'value': 8080}]
50
-
51
- patch.apply(src) == dst # True; src is not modified
52
- jsonpatch.apply_patch(src, [{"op": "remove", "path": "/ports/0"}])
53
- # {'name': 'web', 'replicas': 3, 'ports': [443]}
54
- ```
55
-
56
- Python 3.12+ (CPython). Prebuilt wheels for Linux (x86_64 and aarch64, glibc and
57
- musl), macOS (Apple Silicon and Intel) and Windows (x64); one wheel per platform covers
58
- every Python from 3.12 on. The only dependency is `jsonpointer`.
59
-
60
- ## Why
61
-
62
- JSON Patch describes a change to a JSON document as a list of operations (`add`,
63
- `remove`, `replace`, `move`, `copy`, `test`). It is a media type for HTTP `PATCH`
64
- requests (`application/json-patch+json`), Kubernetes accepts it
65
- (`kubectl patch --type=json`), and it can store or send the difference between two
66
- versions of a document.
67
-
68
- jsonpatch is written in pure Python. In the measurements below:
69
-
70
- - `make_patch` on a 10,000-record document takes 290 ms, and diffing two Kubernetes
71
- OpenAPI specs (3.7 MB and 3.8 MB) takes 136 ms.
72
- - `apply` deep-copies the whole document first, so a 3-operation patch on the
73
- 3.8 MB Kubernetes spec takes 71 ms, against 21 µs when applied in place.
74
- - Applying or computing a patch on a small REST resource takes 47-74 µs per call.
75
-
76
- fastjsonpatch keeps jsonpatch's behaviour exactly and moves these hot paths to Rust,
77
- so existing code gets faster without changes.
78
-
79
- ## Speed
80
-
81
- Measured on Linux x86_64, Python 3.13 (times are the best of several runs).
82
-
83
- Real-world data:
84
-
85
- | scenario | size | jsonpatch | fastjsonpatch | speedup |
86
- |---|---|---|---|---|
87
- | Kubernetes OpenAPI v1.33 (3.7 MB) → v1.34 (3.8 MB): make_patch | 283 ops | 136.4 ms | 9.6 ms | **14x** |
88
- | Kubernetes OpenAPI v1.33 → v1.34: apply | 283 ops | 78.0 ms | 11.4 ms | **7x** |
89
- | 3-op patch on the v1.34 spec | 3.8 MB | 71.1 ms | 10.1 ms | **7x** |
90
- | 3-op patch on the v1.34 spec, `in_place=True` | 3.8 MB | 21.2 µs | 4.7 µs | **4x** |
91
- | PyPI metadata snapshots (8k records): make_patch | 2,487 ops | 255.2 ms | 12.1 ms | **21x** |
92
- | PyPI metadata snapshots: apply | 2,487 ops | 91.7 ms | 14.0 ms | **7x** |
93
- | REST resource, apply_patch per request | ×1000 | 47.4 ms | 7.5 ms | **6x** |
94
- | REST resource, make_patch per request | ×1000 | 74.4 ms | 5.6 ms | **13x** |
95
-
96
- Synthetic records shaped like `{"id", "name", "score", "tags": [..], "meta": {..}}`:
97
-
98
- | workload | ops | make_patch: jsonpatch | fastjsonpatch | speedup | apply: jsonpatch | fastjsonpatch | speedup |
99
- |---|---|---|---|---|---|---|---|
100
- | 10k records, 100 field edits | 100 | 292.61 ms | 5.26 ms | **56x** | 89.38 ms | 7.90 ms | **11x** |
101
- | 10k records, insert 1 at front | 49911 | 669.77 ms | 42.78 ms | **16x** | 535.21 ms | 72.35 ms | **7x** |
102
- | 10k records, delete 50 | 44900 | 636.53 ms | 40.46 ms | **16x** | 563.20 ms | 63.45 ms | **9x** |
103
- | 10k records, move 20 | 26305 | 434.93 ms | 24.38 ms | **18x** | 325.64 ms | 41.03 ms | **8x** |
104
- | 2k ints, insert 1 at front | 1 | 17.04 ms | 1.91 ms | **9x** | 581 µs | 21 µs | **27x** |
105
- | small config (200 keys), 1 edit | 1 | 1.98 ms | 35 µs | **57x** | 464 µs | 32 µs | **15x** |
106
-
107
- Python 3.14 made `copy.deepcopy` about 1.5x faster, so on 3.14 jsonpatch's `apply` on
108
- a large document is faster too; on the "10k records, 100 field edits" workload the
109
- `apply` speedup drops from about 11x to about 6x. The `make_patch` speedup stays about
110
- the same. Run `python bench/compare.py` (from the source distribution) to measure on
111
- your own machine.
112
-
113
- ## Why it is faster
114
-
115
- **make_patch.** jsonpatch walks both documents in Python and compares every scalar
116
- with `json.dumps(a) == json.dumps(b)`. Profiling the "10k records" workload with
117
- cProfile puts about 40% of the time in `json.dumps` and about a third in `isinstance`
118
- checks against abstract base classes (`MutableMapping`, `MutableSequence`).
119
- fastjsonpatch walks the documents in Rust over the CPython objects directly (no
120
- conversion to a Rust data structure), compares values natively with the same rules as the `json.dumps` comparison (`1`, `1.0` and `True`
121
- are all different, `-0.0 != 0.0`, NaN equals NaN), keeps the current path on a stack,
122
- and takes a single lockstep pass when two dicts have the same keys in the same order.
123
- It is a port of jsonpatch's `DiffBuilder`, so it produces the same operations in the
124
- same order, including detected moves.
125
-
126
- **apply.** jsonpatch first runs `copy.deepcopy` on the document (so the original is
127
- not modified), then builds a `JsonPointer` and an operation object for each operation.
128
- For a small patch on a large document the copy is nearly all of the time.
129
- fastjsonpatch copies each dict and list with one C call and only recurses into values
130
- that are themselves containers; it keeps `deepcopy`'s semantics (shared and cyclic
131
- references are copied once) but skips the bookkeeping for containers referenced only
132
- once. It then resolves the paths and runs the operations without creating
133
- `JsonPointer` or operation objects.
134
-
135
- ## Compatibility
136
-
137
- fastjsonpatch is jsonpatch's own module (BSD licensed) with three methods routed to
138
- Rust: `JsonPatch.__init__` (validation), `JsonPatch.from_diff` (`make_patch`) and
139
- `JsonPatch.apply` (`apply_patch`). Everything else is jsonpatch's code, so the public
140
- names are the same: `JsonPatch`, `make_patch`, `apply_patch`, `JsonPatch.from_string`,
141
- the operation classes, `DiffBuilder`, `multidict`, and the exceptions
142
- `JsonPatchException`, `InvalidJsonPatch`, `JsonPatchConflict`, `JsonPatchTestFailed`
143
- (with the same hierarchy, and `JsonPointerException` from jsonpointer).
144
-
145
- The Rust code handles the common case and hands anything else back to the original
146
- Python code, so results never differ:
147
-
148
- - **make_patch**: values it cannot reproduce exactly (non-JSON types that need
149
- comparing, dict or list subclasses, a custom `dumps=`, nesting deeper than 1000)
150
- are diffed by the original `DiffBuilder`.
151
- - **apply**: works on plain dicts and lists. An operation that would fail, or that
152
- touches anything else, is run by the original operation classes before anything is
153
- mutated, so errors have the same type and message.
154
- - `apply` and validation use the original code for a custom `pointer_cls`, a
155
- subclass with custom `operations` or an overridden `_get_operation`, and patches
156
- that are not lists or tuples. `make_patch` uses it for a custom `dumps=` or
157
- `json_dumper`.
158
-
159
- Known differences:
160
-
161
- - The exception classes belong to fastjsonpatch. An exception raised by the original
162
- `jsonpatch` package (for example inside another library that imports it) is not
163
- caught by `except fastjsonpatch.JsonPatchConflict`, and vice versa.
164
- - A value that is not a dict, list or JSON scalar inside a document being applied to
165
- is copied with `copy.deepcopy` on its own, so aliasing between such values is not
166
- preserved.
167
- - `__version__` is fastjsonpatch's version, and jsonpatch's `__author__` and
168
- `__website__` are not defined.
169
-
170
- `fastjsonpatch.__jsonpatch_version__` says which jsonpatch release the behaviour
171
- matches (currently `1.34`).
172
-
173
- ## How it is tested
174
-
175
- About 1,570 tests. CI runs them with Python 3.12, 3.13 and 3.14 on Linux (x86_64 and
176
- aarch64, glibc), macOS (arm64 and x86_64) and Windows (x64); the musl wheels are built
177
- but not tested in CI:
178
-
179
- - **Parity:** random documents, mutations and patches (valid and invalid) run through
180
- both fastjsonpatch and jsonpatch 1.34, comparing the resulting patches and documents
181
- by `repr` and errors by type and message. Also covers aliasing, cycles, non-JSON
182
- values, subclasses, deep nesting and every fallback path.
183
- - **jsonpatch's own test suite** and doctests, run against fastjsonpatch.
184
- - **RFC 6902 conformance:** the
185
- [json-patch-tests](https://github.com/json-patch/json-patch-tests) suite.
186
-
187
- The source distribution includes the tests and the benchmark.
188
-
189
- ## Development
190
-
191
- ```sh
192
- pip install maturin
193
- maturin develop --release
194
- pip install "jsonpatch==1.34" pytest # jsonpatch is the reference for the parity tests
195
- pytest tests
196
- python bench/compare.py # speed vs jsonpatch on this machine
197
- ```
198
-
199
- See [MAINTAINING.md](https://github.com/amzntiger/fastjsonpatch/blob/main/MAINTAINING.md) for releases and upkeep.
200
-
201
- ## License
202
-
203
- BSD-3-Clause. The Python module and the diff algorithm come from jsonpatch
204
- (Copyright (c) 2011 Stefan Kögl, BSD). The licenses of the Rust crates compiled into
205
- the wheels are in `THIRD_PARTY_LICENSES.md`.
206
-
@@ -1,178 +0,0 @@
1
- # fastjsonpatch
2
-
3
- A drop-in, faster [jsonpatch](https://github.com/stefankoegl/python-json-patch)
4
- ([RFC 6902](https://www.rfc-editor.org/rfc/rfc6902) JSON Patch) for Python.
5
- `make_patch` and `apply` run in Rust and return the **same patches and the same
6
- documents, and raise exceptions with the same types and messages,** as jsonpatch 1.34.
7
- In the benchmarks below, making a patch is 9-57x faster and applying one 6-27x faster.
8
-
9
- ```sh
10
- pip install fastjsonpatch
11
- ```
12
-
13
- ```python
14
- import fastjsonpatch as jsonpatch # the only line you change
15
-
16
- src = {"name": "web", "replicas": 3, "ports": [80, 443]}
17
- dst = {"name": "web", "replicas": 5, "ports": [80, 443, 8080]}
18
-
19
- patch = jsonpatch.make_patch(src, dst)
20
- patch.patch
21
- # [{'op': 'replace', 'path': '/replicas', 'value': 5},
22
- # {'op': 'add', 'path': '/ports/2', 'value': 8080}]
23
-
24
- patch.apply(src) == dst # True; src is not modified
25
- jsonpatch.apply_patch(src, [{"op": "remove", "path": "/ports/0"}])
26
- # {'name': 'web', 'replicas': 3, 'ports': [443]}
27
- ```
28
-
29
- Python 3.12+ (CPython). Prebuilt wheels for Linux (x86_64 and aarch64, glibc and
30
- musl), macOS (Apple Silicon and Intel) and Windows (x64); one wheel per platform covers
31
- every Python from 3.12 on. The only dependency is `jsonpointer`.
32
-
33
- ## Why
34
-
35
- JSON Patch describes a change to a JSON document as a list of operations (`add`,
36
- `remove`, `replace`, `move`, `copy`, `test`). It is a media type for HTTP `PATCH`
37
- requests (`application/json-patch+json`), Kubernetes accepts it
38
- (`kubectl patch --type=json`), and it can store or send the difference between two
39
- versions of a document.
40
-
41
- jsonpatch is written in pure Python. In the measurements below:
42
-
43
- - `make_patch` on a 10,000-record document takes 290 ms, and diffing two Kubernetes
44
- OpenAPI specs (3.7 MB and 3.8 MB) takes 136 ms.
45
- - `apply` deep-copies the whole document first, so a 3-operation patch on the
46
- 3.8 MB Kubernetes spec takes 71 ms, against 21 µs when applied in place.
47
- - Applying or computing a patch on a small REST resource takes 47-74 µs per call.
48
-
49
- fastjsonpatch keeps jsonpatch's behaviour exactly and moves these hot paths to Rust,
50
- so existing code gets faster without changes.
51
-
52
- ## Speed
53
-
54
- Measured on Linux x86_64, Python 3.13 (times are the best of several runs).
55
-
56
- Real-world data:
57
-
58
- | scenario | size | jsonpatch | fastjsonpatch | speedup |
59
- |---|---|---|---|---|
60
- | Kubernetes OpenAPI v1.33 (3.7 MB) → v1.34 (3.8 MB): make_patch | 283 ops | 136.4 ms | 9.6 ms | **14x** |
61
- | Kubernetes OpenAPI v1.33 → v1.34: apply | 283 ops | 78.0 ms | 11.4 ms | **7x** |
62
- | 3-op patch on the v1.34 spec | 3.8 MB | 71.1 ms | 10.1 ms | **7x** |
63
- | 3-op patch on the v1.34 spec, `in_place=True` | 3.8 MB | 21.2 µs | 4.7 µs | **4x** |
64
- | PyPI metadata snapshots (8k records): make_patch | 2,487 ops | 255.2 ms | 12.1 ms | **21x** |
65
- | PyPI metadata snapshots: apply | 2,487 ops | 91.7 ms | 14.0 ms | **7x** |
66
- | REST resource, apply_patch per request | ×1000 | 47.4 ms | 7.5 ms | **6x** |
67
- | REST resource, make_patch per request | ×1000 | 74.4 ms | 5.6 ms | **13x** |
68
-
69
- Synthetic records shaped like `{"id", "name", "score", "tags": [..], "meta": {..}}`:
70
-
71
- | workload | ops | make_patch: jsonpatch | fastjsonpatch | speedup | apply: jsonpatch | fastjsonpatch | speedup |
72
- |---|---|---|---|---|---|---|---|
73
- | 10k records, 100 field edits | 100 | 292.61 ms | 5.26 ms | **56x** | 89.38 ms | 7.90 ms | **11x** |
74
- | 10k records, insert 1 at front | 49911 | 669.77 ms | 42.78 ms | **16x** | 535.21 ms | 72.35 ms | **7x** |
75
- | 10k records, delete 50 | 44900 | 636.53 ms | 40.46 ms | **16x** | 563.20 ms | 63.45 ms | **9x** |
76
- | 10k records, move 20 | 26305 | 434.93 ms | 24.38 ms | **18x** | 325.64 ms | 41.03 ms | **8x** |
77
- | 2k ints, insert 1 at front | 1 | 17.04 ms | 1.91 ms | **9x** | 581 µs | 21 µs | **27x** |
78
- | small config (200 keys), 1 edit | 1 | 1.98 ms | 35 µs | **57x** | 464 µs | 32 µs | **15x** |
79
-
80
- Python 3.14 made `copy.deepcopy` about 1.5x faster, so on 3.14 jsonpatch's `apply` on
81
- a large document is faster too; on the "10k records, 100 field edits" workload the
82
- `apply` speedup drops from about 11x to about 6x. The `make_patch` speedup stays about
83
- the same. Run `python bench/compare.py` (from the source distribution) to measure on
84
- your own machine.
85
-
86
- ## Why it is faster
87
-
88
- **make_patch.** jsonpatch walks both documents in Python and compares every scalar
89
- with `json.dumps(a) == json.dumps(b)`. Profiling the "10k records" workload with
90
- cProfile puts about 40% of the time in `json.dumps` and about a third in `isinstance`
91
- checks against abstract base classes (`MutableMapping`, `MutableSequence`).
92
- fastjsonpatch walks the documents in Rust over the CPython objects directly (no
93
- conversion to a Rust data structure), compares values natively with the same rules as the `json.dumps` comparison (`1`, `1.0` and `True`
94
- are all different, `-0.0 != 0.0`, NaN equals NaN), keeps the current path on a stack,
95
- and takes a single lockstep pass when two dicts have the same keys in the same order.
96
- It is a port of jsonpatch's `DiffBuilder`, so it produces the same operations in the
97
- same order, including detected moves.
98
-
99
- **apply.** jsonpatch first runs `copy.deepcopy` on the document (so the original is
100
- not modified), then builds a `JsonPointer` and an operation object for each operation.
101
- For a small patch on a large document the copy is nearly all of the time.
102
- fastjsonpatch copies each dict and list with one C call and only recurses into values
103
- that are themselves containers; it keeps `deepcopy`'s semantics (shared and cyclic
104
- references are copied once) but skips the bookkeeping for containers referenced only
105
- once. It then resolves the paths and runs the operations without creating
106
- `JsonPointer` or operation objects.
107
-
108
- ## Compatibility
109
-
110
- fastjsonpatch is jsonpatch's own module (BSD licensed) with three methods routed to
111
- Rust: `JsonPatch.__init__` (validation), `JsonPatch.from_diff` (`make_patch`) and
112
- `JsonPatch.apply` (`apply_patch`). Everything else is jsonpatch's code, so the public
113
- names are the same: `JsonPatch`, `make_patch`, `apply_patch`, `JsonPatch.from_string`,
114
- the operation classes, `DiffBuilder`, `multidict`, and the exceptions
115
- `JsonPatchException`, `InvalidJsonPatch`, `JsonPatchConflict`, `JsonPatchTestFailed`
116
- (with the same hierarchy, and `JsonPointerException` from jsonpointer).
117
-
118
- The Rust code handles the common case and hands anything else back to the original
119
- Python code, so results never differ:
120
-
121
- - **make_patch**: values it cannot reproduce exactly (non-JSON types that need
122
- comparing, dict or list subclasses, a custom `dumps=`, nesting deeper than 1000)
123
- are diffed by the original `DiffBuilder`.
124
- - **apply**: works on plain dicts and lists. An operation that would fail, or that
125
- touches anything else, is run by the original operation classes before anything is
126
- mutated, so errors have the same type and message.
127
- - `apply` and validation use the original code for a custom `pointer_cls`, a
128
- subclass with custom `operations` or an overridden `_get_operation`, and patches
129
- that are not lists or tuples. `make_patch` uses it for a custom `dumps=` or
130
- `json_dumper`.
131
-
132
- Known differences:
133
-
134
- - The exception classes belong to fastjsonpatch. An exception raised by the original
135
- `jsonpatch` package (for example inside another library that imports it) is not
136
- caught by `except fastjsonpatch.JsonPatchConflict`, and vice versa.
137
- - A value that is not a dict, list or JSON scalar inside a document being applied to
138
- is copied with `copy.deepcopy` on its own, so aliasing between such values is not
139
- preserved.
140
- - `__version__` is fastjsonpatch's version, and jsonpatch's `__author__` and
141
- `__website__` are not defined.
142
-
143
- `fastjsonpatch.__jsonpatch_version__` says which jsonpatch release the behaviour
144
- matches (currently `1.34`).
145
-
146
- ## How it is tested
147
-
148
- About 1,570 tests. CI runs them with Python 3.12, 3.13 and 3.14 on Linux (x86_64 and
149
- aarch64, glibc), macOS (arm64 and x86_64) and Windows (x64); the musl wheels are built
150
- but not tested in CI:
151
-
152
- - **Parity:** random documents, mutations and patches (valid and invalid) run through
153
- both fastjsonpatch and jsonpatch 1.34, comparing the resulting patches and documents
154
- by `repr` and errors by type and message. Also covers aliasing, cycles, non-JSON
155
- values, subclasses, deep nesting and every fallback path.
156
- - **jsonpatch's own test suite** and doctests, run against fastjsonpatch.
157
- - **RFC 6902 conformance:** the
158
- [json-patch-tests](https://github.com/json-patch/json-patch-tests) suite.
159
-
160
- The source distribution includes the tests and the benchmark.
161
-
162
- ## Development
163
-
164
- ```sh
165
- pip install maturin
166
- maturin develop --release
167
- pip install "jsonpatch==1.34" pytest # jsonpatch is the reference for the parity tests
168
- pytest tests
169
- python bench/compare.py # speed vs jsonpatch on this machine
170
- ```
171
-
172
- See [MAINTAINING.md](https://github.com/amzntiger/fastjsonpatch/blob/main/MAINTAINING.md) for releases and upkeep.
173
-
174
- ## License
175
-
176
- BSD-3-Clause. The Python module and the diff algorithm come from jsonpatch
177
- (Copyright (c) 2011 Stefan Kögl, BSD). The licenses of the Rust crates compiled into
178
- the wheels are in `THIRD_PARTY_LICENSES.md`.
File without changes
File without changes