Hyperlinkv0.8.0-beta.28

JsonPatch

JsonPatch.getfunctioneffect/JsonPatch.ts:182
(oldValue: Schema.Json, newValue: Schema.Json): JsonPatch

Computes a structural patch that transforms oldValue into newValue.

When to use

Use to compute a JSON Patch from before and after JSON documents, detect structural changes, or create deterministic update operations.

Details

Generates a structural diff between two JSON values, producing a patch that yields newValue when applied to oldValue. It returns an empty array when values are identical, recursively diffs nested structures, emits root replace operations for primitive changes, and processes object keys in sorted order for stable output.

Gotchas

Arrays are compared by index position, with no move or copy detection. Array removals are emitted from highest to lowest index to prevent index shifting. The output is deterministic but not guaranteed to be minimal.

Example (Computing object diff)

import { JsonPatch } from "effect"

const oldValue = { users: [{ id: 1, name: "Alice" }], count: 1 }
const newValue = { users: [{ id: 1, name: "Bob" }, { id: 2, name: "Charlie" }], count: 2 }

const patch = JsonPatch.get(oldValue, newValue)
// [
//   { op: "replace", path: "/users/0/name", value: "Bob" },
//   { op: "add", path: "/users/1", value: { id: 2, name: "Charlie" } },
//   { op: "replace", path: "/count", value: 2 }
// ]
export function get(oldValue: Schema.Json, newValue: Schema.Json): JsonPatch {
  if (Object.is(oldValue, newValue)) return []
  const patches: Array<JsonPatchOperation> = []

  if (Array.isArray(oldValue) && Array.isArray(newValue)) {
    const len1 = oldValue.length
    const len2 = newValue.length

    // Compare shared prefix by index
    const shared = Math.min(len1, len2)
    for (let i = 0; i < shared; i++) {
      const path = `/${i}`
      const patch = get(oldValue[i], newValue[i])
      for (const op of patch) {
        prefixPathInPlace(op, path)
        patches.push(op)
      }
    }

    // Remove from end to start so later indices do not shift.
    for (let i = len1 - 1; i >= len2; i--) {
      patches.push({ op: "remove", path: `/${i}` })
    }

    // Add from beginning to end.
    for (let i = len1; i < len2; i++) {
      patches.push({ op: "add", path: `/${i}`, value: newValue[i] })
    }

    return patches
  }

  if (isJsonObject(oldValue) && isJsonObject(newValue)) {
    const keys1 = Object.keys(oldValue)
    const keys2 = Object.keys(newValue)
    const allKeys = Array.from(new Set([...keys1, ...keys2])).sort()

    for (const key of allKeys) {
      const esc = escapeToken(key)
      const path = `/${esc}`
      const hasKey1 = Object.hasOwn(oldValue, key)
      const hasKey2 = Object.hasOwn(newValue, key)

      if (hasKey1 && hasKey2) {
        const patch = get(oldValue[key], newValue[key])
        for (const op of patch) {
          prefixPathInPlace(op, path)
          patches.push(op)
        }
      } else if (!hasKey1 && hasKey2) {
        patches.push({ op: "add", path, value: newValue[key] })
      } else if (hasKey1 && !hasKey2) {
        patches.push({ op: "remove", path })
      }
    }

    return patches
  }

  patches.push({ op: "replace", path: "", value: newValue })
  return patches
}
Referenced by 1 symbols