formats: JSON Patch #81

Closed
opened 2026-10-05 21:07:17 +00:00 by orion · 8 comments
Collaborator

Context

JSON Patch as format packages: RFC 6902 (JSON Patch: add, remove, replace, move, copy, test) and JSON Merge Patch (RFC 7396), both in the versions of the ROADMAP table. A patch document is itself JSON, so it is a type on Encode/Decode; applying it works on JsonValue. This is an epic: a pointer type, two patch formats, two public test oracles and docs are more than one PR. Steps become issues just in time, one at a time.

Source

  • ROADMAP.md, format versions: JSON Patch is RFC 6902, and JSON Merge Patch (RFC 7396); the table lists no unsupported versions.
  • ENCODING.md section 9: formats are packages on Encode and Decode.
  • std/json (JsonValue) is the document type; std/toml, std/xml and std/yaml are the layout model (format value, parser, vendored public suite, docs page).

Design record point

ROADMAP "Which versions a format supports" (JSON Patch row) and ENCODING.md section 9. Decided by the loop:

  • RFC 6901 JSON Pointer is a prerequisite of RFC 6902 and is shared with the reference step of #80. It becomes JsonPointer in std/json (step 1), not a package of its own; #80 expects it there.
  • RFC 6902 and RFC 7396 are one package, std/json-patch (hyphenated like std/json-schema), pure TorbScript on JsonValue and JsonPointer, not in the prelude. They are two operations on one family of documents and share the application engine and the docs page; separate packages would split one small subject in two.
  • JsonPatch (a list of operations) is a value that is Encode/Decode; the six operations are cases of one PatchOperation type. Applying is a method, JsonPatch.apply(document: JsonValue): Result<JsonValue, PatchError>, and atomic as RFC 6902 section 5 requires: a failing operation, or a failing test, returns the error and no partial document. JsonValue is a value, so "the original is left untouched" holds by construction. PatchError names the operation index and the reason.
  • Merge Patch is MergePatch over JsonValue (MergePatch.apply(target), with the RFC's null deletes a member, non-object patch replaces).
  • Test oracles: the public json-patch-tests suite (tests.json and spec_tests.json, MIT, vendored as a generated suite-cases.trb with notice and pinned commit, the model is std/yaml/tests/suite-cases.trb) for RFC 6902, and the RFC 7396 appendix A table (15 cases, public text of the RFC) for Merge Patch. Cases that need arbitrary-precision numbers (JsonValue.Number is a Float64) are excluded by name with the reason.
  • Out of scope: generating a patch from two documents (diff), and patching typed values through Describe; if wanted, they are their own issues later.
  • Touches the seed? No for the whole epic: the compiler does not import std/json-patch, and the addition to std/json is a new public type that no std name lookup by string refers to. A step that changes that carries the two-commit rule itself.

Steps

  • 1. #435 JsonPointer (RFC 6901) in std/json: string and URI-fragment forms, JsonValue.get, errors, docs section, tests from the RFC's examples
  • 2. #581 std/json-patch with RFC 6902: JsonPatch/PatchOperation on Encode/Decode, atomic apply (add incl. the - index, remove, replace, move, copy, test with the RFC's equality rules), PatchError; vendored json-patch-tests suite, docs page docs/standard-library/json-patch.md, ROADMAP row marked started
  • 3. RFC 7396 Merge Patch in std/json-patch: MergePatch.apply, the RFC's appendix A cases as tests, docs page extended, ROADMAP row marked done (closes the epic's acceptance)

Acceptance criteria (epic)

  • The packages on Encode and Decode in the versions of the table (RFC 6902 and RFC 7396)
  • The format's public test suite passes (json-patch-tests; the RFC 7396 appendix cases), with no exclusion left that is not a documented non-goal (arbitrary-precision numbers)
  • A page under docs/standard-library/

Dependencies

  • Needs: std/json (JsonValue), std/test, all built.
  • Shares JsonPointer with the reference step of #80 (whichever lands first adds it to std/json); otherwise independent of #80, #434, #78 and #79.

Moved from the planning lists of the docs; the design record stays the specification.

## Context JSON Patch as format packages: RFC 6902 (JSON Patch: `add`, `remove`, `replace`, `move`, `copy`, `test`) and JSON Merge Patch (RFC 7396), both in the versions of the ROADMAP table. A patch document is itself JSON, so it is a type on `Encode`/`Decode`; applying it works on `JsonValue`. This is an epic: a pointer type, two patch formats, two public test oracles and docs are more than one PR. Steps become issues just in time, one at a time. ## Source - [ROADMAP.md, format versions](https://git.torb.dev/torbscript/language/src/commit/393ef2608e4a60b8ff76d8b6b408951b822d887d/docs/ROADMAP.md#L185): JSON Patch is RFC 6902, and JSON Merge Patch (RFC 7396); the table lists no unsupported versions. - [ENCODING.md section 9](https://git.torb.dev/torbscript/language/src/commit/393ef2608e4a60b8ff76d8b6b408951b822d887d/docs/design/ENCODING.md#L709): formats are packages on `Encode` and `Decode`. - `std/json` (`JsonValue`) is the document type; `std/toml`, `std/xml` and `std/yaml` are the layout model (format value, parser, vendored public suite, docs page). ## Design record point ROADMAP "Which versions a format supports" (JSON Patch row) and ENCODING.md section 9. Decided by the loop: - RFC 6901 JSON Pointer is a prerequisite of RFC 6902 and is shared with the reference step of #80. It becomes `JsonPointer` in `std/json` (step 1), not a package of its own; #80 expects it there. - RFC 6902 and RFC 7396 are one package, `std/json-patch` (hyphenated like `std/json-schema`), pure TorbScript on `JsonValue` and `JsonPointer`, not in the prelude. They are two operations on one family of documents and share the application engine and the docs page; separate packages would split one small subject in two. - `JsonPatch` (a list of operations) is a value that is `Encode`/`Decode`; the six operations are cases of one `PatchOperation` type. Applying is a method, `JsonPatch.apply(document: JsonValue): Result<JsonValue, PatchError>`, and atomic as RFC 6902 section 5 requires: a failing operation, or a failing `test`, returns the error and no partial document. `JsonValue` is a value, so "the original is left untouched" holds by construction. `PatchError` names the operation index and the reason. - Merge Patch is `MergePatch` over `JsonValue` (`MergePatch.apply(target)`, with the RFC's `null` deletes a member, non-object patch replaces). - Test oracles: the public json-patch-tests suite (`tests.json` and `spec_tests.json`, MIT, vendored as a generated `suite-cases.trb` with notice and pinned commit, the model is `std/yaml/tests/suite-cases.trb`) for RFC 6902, and the RFC 7396 appendix A table (15 cases, public text of the RFC) for Merge Patch. Cases that need arbitrary-precision numbers (`JsonValue.Number` is a `Float64`) are excluded by name with the reason. - Out of scope: generating a patch from two documents (`diff`), and patching typed values through `Describe`; if wanted, they are their own issues later. - Touches the seed? No for the whole epic: the compiler does not import `std/json-patch`, and the addition to `std/json` is a new public type that no std name lookup by string refers to. A step that changes that carries the two-commit rule itself. ## Steps - [x] 1. #435 `JsonPointer` (RFC 6901) in `std/json`: string and URI-fragment forms, `JsonValue.get`, errors, docs section, tests from the RFC's examples - [x] 2. #581 `std/json-patch` with RFC 6902: `JsonPatch`/`PatchOperation` on `Encode`/`Decode`, atomic `apply` (`add` incl. the `-` index, `remove`, `replace`, `move`, `copy`, `test` with the RFC's equality rules), `PatchError`; vendored json-patch-tests suite, docs page `docs/standard-library/json-patch.md`, ROADMAP row marked started - [x] 3. RFC 7396 Merge Patch in `std/json-patch`: `MergePatch.apply`, the RFC's appendix A cases as tests, docs page extended, ROADMAP row marked done (closes the epic's acceptance) ## Acceptance criteria (epic) - [x] The packages on `Encode` and `Decode` in the versions of the table (RFC 6902 and RFC 7396) - [x] The format's public test suite passes (json-patch-tests; the RFC 7396 appendix cases), with no exclusion left that is not a documented non-goal (arbitrary-precision numbers) - [x] A page under `docs/standard-library/` ## Dependencies - Needs: `std/json` (`JsonValue`), `std/test`, all built. - Shares `JsonPointer` with the reference step of #80 (whichever lands first adds it to `std/json`); otherwise independent of #80, #434, #78 and #79. _Moved from the planning lists of the docs; the design record stays the specification._
Author
Collaborator

[loop:torbscript] intake: in scope (author orion, label loop), sources ROADMAP.md format versions and ENCODING.md section 9, not a duplicate, no collision with a design record. TorbScript package, public test suite in forge CI, no seed impact expected. next: refine, waiting (ready buffer full: #4, #163, #164, #223 are ready) until 2026-10-10T04:00:00Z

[loop:torbscript] intake: in scope (author orion, label loop), sources ROADMAP.md format versions and ENCODING.md section 9, not a duplicate, no collision with a design record. TorbScript package, public test suite in forge CI, no seed impact expected. next: refine, waiting (ready buffer full: #4, #163, #164, #223 are ready) until 2026-10-10T04:00:00Z
Author
Collaborator

[loop:torbscript] claim refine by session:loop-torbscript-20261023 until 2026-10-23T17:22Z

[loop:torbscript] claim refine by session:loop-torbscript-20261023 until 2026-10-23T17:22Z
Author
Collaborator

[loop:torbscript] decided by the loop: #81 is too big for one PR, so it is an epic and stays without ready. It needs a pointer type (RFC 6901), the six RFC 6902 operations with atomic application, RFC 7396 Merge Patch, two public test oracles and the docs, and it shares the pointer with the reference step of #80. Order follows the dependencies: JsonPointer first (it unblocks both #81 and #80), then RFC 6902, then Merge Patch. Naming and placement follow the neighbours: JsonPointer goes into std/json as #80 already expects (re-exported from a new file, like std/toml), patches live in std/json-patch (hyphenated like std/json-schema), methods not free functions (#146). Test oracles: the public json-patch-tests suite (MIT) vendored as a generated suite-cases.trb like std/yaml, and the appendix cases of RFC 7396; arbitrary-precision numbers are excluded by name because JsonValue.Number is a Float64. diff generation and typed patching are out of scope. No seed touch for the whole epic (the compiler imports std/json but looks up no new name by string; additions are public declarations only). Step 1 is filed as #435 (keyword search of open and closed issues for pointer, patch, RFC 6901/6902, merge patch and std/json found only #80, #434 and this one; none duplicates it) and is ready; steps 2 and 3 are filed just in time. Dependencies: #435 is shared with #80's reference step; otherwise independent of #434.

[loop:torbscript] refine: released the claim of session:loop-torbscript-20261023. #81 is an epic (checklist of 3 steps in the body), step 1 filed as #435 with labels loop and ready. Nothing was written to the repo. Worker usage: about 85k tokens (sonnet).
next: waiting (epic waits on its steps)

[loop:torbscript] decided by the loop: #81 is too big for one PR, so it is an epic and stays without `ready`. It needs a pointer type (RFC 6901), the six RFC 6902 operations with atomic application, RFC 7396 Merge Patch, two public test oracles and the docs, and it shares the pointer with the reference step of #80. Order follows the dependencies: `JsonPointer` first (it unblocks both #81 and #80), then RFC 6902, then Merge Patch. Naming and placement follow the neighbours: `JsonPointer` goes into `std/json` as #80 already expects (re-exported from a new file, like `std/toml`), patches live in `std/json-patch` (hyphenated like `std/json-schema`), methods not free functions (#146). Test oracles: the public json-patch-tests suite (MIT) vendored as a generated `suite-cases.trb` like `std/yaml`, and the appendix cases of RFC 7396; arbitrary-precision numbers are excluded by name because `JsonValue.Number` is a `Float64`. `diff` generation and typed patching are out of scope. No seed touch for the whole epic (the compiler imports `std/json` but looks up no new name by string; additions are public declarations only). Step 1 is filed as #435 (keyword search of open and closed issues for pointer, patch, RFC 6901/6902, merge patch and std/json found only #80, #434 and this one; none duplicates it) and is `ready`; steps 2 and 3 are filed just in time. Dependencies: #435 is shared with #80's reference step; otherwise independent of #434. [loop:torbscript] refine: released the claim of session:loop-torbscript-20261023. #81 is an epic (checklist of 3 steps in the body), step 1 filed as #435 with labels `loop` and `ready`. Nothing was written to the repo. Worker usage: about 85k tokens (sonnet). next: waiting (epic waits on its steps)
Author
Collaborator

[loop:torbscript] claim refine by session:loop-torbscript-20261029 until 2026-10-29T13:51Z

[loop:torbscript] claim refine by session:loop-torbscript-20261029 until 2026-10-29T13:51Z
Author
Collaborator

[loop:torbscript] decided by the loop: step 1 (#435) landed on main (PR #437, merge commit b929c58; std/json/src/pointer.trb with JsonPointer and JsonValue.get, verified on origin/main), so step 2 starts now. Filed as #581 (keyword search of open and closed issues for json-patch, JsonPointer, pointer, patch and merge patch found only #80, #81, #434 and #435; no duplicate), with loop and ready. Sizing: the package, the six operations with atomic apply, Encode/Decode, the docs page and the vendored json-patch-tests suite are one PR in two Conventional Commits (feature plus hand tests and docs; then the generated suite with its exclusions and the ROADMAP row), the same split std/json-schema used. Dependencies are met: JsonPointer is on main, std/json and std/test are built. No seed touch. Step 3 (RFC 7396) is filed when #581 lands. The epic checklist now ticks step 1 and links #581.

[loop:torbscript] refine: step 2 filed as #581 and set ready; #81 stays an epic without ready. Nothing was written to the repo. Worker usage: about 60k tokens (sonnet).
next: waiting (epic waits on its steps; #581 is ready to implement)

[loop:torbscript] decided by the loop: step 1 (#435) landed on `main` (PR #437, merge commit b929c58; `std/json/src/pointer.trb` with `JsonPointer` and `JsonValue.get`, verified on origin/main), so step 2 starts now. Filed as #581 (keyword search of open and closed issues for json-patch, JsonPointer, pointer, patch and merge patch found only #80, #81, #434 and #435; no duplicate), with `loop` and `ready`. Sizing: the package, the six operations with atomic `apply`, `Encode`/`Decode`, the docs page and the vendored json-patch-tests suite are one PR in two Conventional Commits (feature plus hand tests and docs; then the generated suite with its exclusions and the ROADMAP row), the same split `std/json-schema` used. Dependencies are met: `JsonPointer` is on `main`, `std/json` and `std/test` are built. No seed touch. Step 3 (RFC 7396) is filed when #581 lands. The epic checklist now ticks step 1 and links #581. [loop:torbscript] refine: step 2 filed as #581 and set `ready`; #81 stays an epic without `ready`. Nothing was written to the repo. Worker usage: about 60k tokens (sonnet). next: waiting (epic waits on its steps; #581 is ready to implement)
Author
Collaborator

[loop:torbscript] claim refine by session:loop-torbscript-20261030 @anthropic:theorben until 2026-10-30T12:02Z

[loop:torbscript] claim refine by session:loop-torbscript-20261030 @anthropic:theorben until 2026-10-30T12:02Z
Author
Collaborator

[loop:torbscript] refine: decided by the loop: step 2 (#581) landed on main (PR #584, commits 8acff50 and 16fe80a, std/json-patch with JsonPatch, atomic apply and the json-patch-tests suite, verified on origin/main; issue closed), so step 3 starts now. Step 2 is ticked in the checklist. Filed as #687 (keyword search of open and closed issues for merge patch, 7396 and json-patch found only #80, #81, #435 and #581; no duplicate): MergePatch.apply, the 15 appendix A cases as tests, docs section, ROADMAP row marked done. Labels loop and ready; no seed impact, one PR.

released the claim of session:loop-torbscript-20261030

next: ready (#687)

[loop:torbscript] refine: decided by the loop: step 2 (#581) landed on `main` (PR #584, commits 8acff50 and 16fe80a, `std/json-patch` with `JsonPatch`, atomic `apply` and the json-patch-tests suite, verified on origin/main; issue closed), so step 3 starts now. Step 2 is ticked in the checklist. Filed as #687 (keyword search of open and closed issues for merge patch, 7396 and json-patch found only #80, #81, #435 and #581; no duplicate): `MergePatch.apply`, the 15 appendix A cases as tests, docs section, ROADMAP row marked done. Labels `loop` and `ready`; no seed impact, one PR. released the claim of session:loop-torbscript-20261030 next: ready (#687)
Author
Collaborator

[loop:torbscript] decided by the loop: step 3 (#687) landed on main (PR #692; std/json-patch/src/merge.trb with MergePatch.apply, the appendix A cases as tests, docs page and ROADMAP row 'both done' verified on origin/main). All three steps and the epic's acceptance criteria are met, so the epic is closed. Claim of session:loop-torbscript-20261030 released. next: none (epic done)

[loop:torbscript] decided by the loop: step 3 (#687) landed on `main` (PR #692; `std/json-patch/src/merge.trb` with `MergePatch.apply`, the appendix A cases as tests, docs page and ROADMAP row 'both done' verified on origin/main). All three steps and the epic's acceptance criteria are met, so the epic is closed. Claim of session:loop-torbscript-20261030 released. next: none (epic done)
orion closed this issue 2026-10-30 11:30:01 +00:00
Sign in to join this conversation.
No labels
loop
ready
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language#81
No description provided.