JSON Merge Patch vs. Keyfold
Relevant source files
The following files were used as context for generating this wiki page:
Keyfold is an immutable state tree utility designed to fold partial deltas into a nested state structure. While it shares a lineage with JSON Merge Patch (RFC 7396), it specifically addresses the standard's limitations regarding array handling by introducing identity-keyed list reconciliation. Sources: README.md:1-10, AGENTS.md:33-35
The project provides a mechanism to apply partial updates to plain objects while maintaining structural sharing and deterministic results. Unlike standard merge utilities that might corrupt lists by index, Keyfold uses configurable identity fields to update, delete, or append items within lists. Sources: README.md:20-25, AGENTS.md:11-13
Structural Differences in Logic
The primary architectural divergence between RFC 7396 and Keyfold lies in how they interpret container types and deletions. Keyfold prioritizes "identity" over "position" for arrays. Sources: README.md:6-18
Array Handling
In JSON Merge Patch (RFC 7396), every array is treated as an opaque scalar value; any update replaces the entire array wholesale. Keyfold allows arrays to be configured as "keyed lists" via a keyBy policy. Sources: README.md:14-16, src/reconcile.ts:20-25
The diagram above illustrates how Keyfold decides between wholesale replacement (standard RFC 7396 behavior) and its proprietary keyed reconciliation logic. Sources: src/merge.ts:40-55, src/reconcile.ts:31-50
Deletion Semantics
JSON Merge Patch uses null as a signal to delete a field. Keyfold deliberately keeps null as valid data and uses explicit sentinels for deletion to avoid data loss. Sources: README.md:120-125, src/sentinels.ts:1-5
| Feature | JSON Merge Patch (RFC 7396) | Keyfold |
|---|---|---|
| Delete Field | Set to null | Set to DELETE symbol (or wire token) |
Keep null Value | Impossible (interpreted as delete) | Possible (treated as data) |
| Array Update | Wholesale replacement | Keyed reconciliation or replacement |
| List Ordering | N/A (always replaced) | Preserves base order, appends new items |
| Delete List Item | Replace whole list | { id, $delete: true } tombstone |
Sources: README.md:120-128, src/reconcile.ts:25-30, test/merge.test.ts:42-50
Keyfold Operational Flow
Keyfold operates through a pre-compiled merger function that applies a Delta<T> to a Base: T. This flow ensures that the base is never mutated and structural sharing is maximized for unchanged branches. Sources: src/merge.ts:15-25, AGENTS.md:10-12
The Reconciliation Loop
When a keyed list is encountered, Keyfold follows a specific reconciliation algorithm to ensure idempotency and stable ordering. Sources: src/reconcile.ts:20-65
The sequence diagram demonstrates the "index then iterate" approach used in reconcile.ts to maintain O(list) performance while ensuring the base order of survivors is preserved. Sources: src/reconcile.ts:27-60
Wire Protocol and Tokens
For transport over JSON (where symbols like DELETE cannot exist), Keyfold provides a wireDeletes option. This allows a specific string token to represent a field deletion, mimicking the behavior of null in JSON Merge Patch but without sacrificing the null value itself. Sources: test/wire.test.ts:20-35, src/sentinels.ts:5
- DELETE Symbol: A unique in-memory symbol for programmatic use. Sources: src/sentinels.ts:2
- DELETE_TOKEN: The string
"@@keyfold/delete", interpreted only ifwireDeletes: true. Sources: src/sentinels.ts:5, test/wire.test.ts:25-30 - Item Tombstones: The object
{ id, $delete: true }, which is JSON-native and works regardless of thewireDeletessetting. Sources: test/wire.test.ts:45-50, src/reconcile.ts:133-140
Implementation Constraints
Unlike some merge libraries that traverse the entire state, Keyfold is "delta-proportional." It only visits branches mentioned in the delta, except for keyed lists, which must be scanned to index identities. Sources: AGENTS.md:25-28, README.md:148-152
- Deterministic: Re-applying the same delta returns the previous result by reference. Sources: AGENTS.md:13-14
- Failure-Safe: A throw (e.g., due to duplicate identities) never leaves a partially updated tree. Sources: AGENTS.md:10-11, src/merge.ts:18-20
- Boundary Protection: Paths configured with
replaceor unkeyed arrays act as boundaries where recursion stops. Sources: AGENTS.md:15-18, src/merge.ts:38-39
Conclusion
Keyfold extends the concepts of JSON Merge Patch into complex, keyed state trees. By moving away from RFC 7396's reliance on null for deletion and its wholesale array replacement strategy, Keyfold provides a more granular and safe mechanism for updating nested state in modern application environments like websocket reducers and optimistic UIs. Sources: README.md:20-30, README.md:140-145