Skip to main content
Version: 2.x

JsonPatch

JsonPatch is an untyped, composable patch for Json values. It represents a sequence of operations that transform one Json value into another — computed automatically via a diff algorithm or constructed manually. The two fundamental operations are JsonPatch.diff to compute a patch between two Json values, and JsonPatch#apply to apply it.

JsonPatch:

  • is a pure value — applying it never mutates the input
  • is composable via ++, sequencing two patches one after another
  • supports three failure-handling modes: Strict, Lenient, and Clobber
  • carries its own Schema instances for full serialization support
  • converts bidirectionally to/from DynamicPatch for use in generic patching pipelines

The JsonPatch type wraps a sequence of operations:

final case class JsonPatch(ops: Chunk[JsonPatch.JsonPatchOp])

Motivation

In most systems, updating JSON data means transmitting the entire new value — even when only a single field changed. JsonPatch solves this by representing changes as a first-class value that can be:

  • Transmitted efficiently — send only what changed, not the entire document
  • Stored for audit logs — record every change for compliance, debugging, or undo
  • Composed — merge multiple changes into a single atomic patch
  • Serialized — persist patches to disk or a message queue and replay them later
Source JSON                    Target JSON
┌─────────────────────┐ ┌─────────────────────┐
│ { "name": "Alice", │ │ { "name": "Alice", │
│ "age": 25, │──diff──│ "age": 26, │
│ "city": "NYC" } │ │ "city": "NYC" } │
└─────────────────────┘ └─────────────────────┘


JsonPatch {
ObjectEdit(
Modify("age", NumberDelta(1))
)
}

▼ apply
┌─────────────────────┐
│ { "name": "Alice", │
│ "age": 26, │
│ "city": "NYC" } │
└─────────────────────┘

The "hello world" for JsonPatch is diff-then-apply. We compute a patch from source to target, then verify that applying it to source reproduces target:

import zio.blocks.schema.json.{Json, JsonPatch}

val source = Json.Object("name" -> Json.String("Alice"), "age" -> Json.Number(25))
val target = Json.Object("name" -> Json.String("Alice"), "age" -> Json.Number(26))

val patch: JsonPatch = JsonPatch.diff(source, target)

Applying the patch to source always yields Right(target):

patch.apply(source) == Right(target)
// res1: Boolean = true

Creating Patches

There are three ways to create a JsonPatch: compute one automatically with JsonPatch.diff, construct one manually with JsonPatch.root or JsonPatch.apply, or start from the identity patch JsonPatch.empty.

JsonPatch.diff

Computes the minimal JsonPatch that transforms source into target. Uses a smart diff strategy per value type — see the Diffing Algorithm section for details:

object JsonPatch {
def diff(source: Json, target: Json): JsonPatch
}

JsonPatch.diff is also available as the Json#diff extension method:

import zio.blocks.schema.json.{Json, JsonPatch}

// Via companion object
val p1 = JsonPatch.diff(Json.Number(10), Json.Number(15))

// Via extension method on Json
val p2 = Json.Number(10).diff(Json.Number(15))

// Nested object diff produces minimal ObjectEdit
val p3 = JsonPatch.diff(
Json.Object("a" -> Json.Number(1), "b" -> Json.Number(2)),
Json.Object("a" -> Json.Number(1), "b" -> Json.Number(9))
)
// p3 only touches "b", leaves "a" unchanged

JsonPatch.root

Creates a patch with a single operation applied at the root of the value:

object JsonPatch {
def root(operation: JsonPatch.Op): JsonPatch
}

For example, we can replace the root entirely, increment a number, or add a field to a root object:

import zio.blocks.schema.json.{Json, JsonPatch}
import zio.blocks.schema.json.JsonPatch._
import zio.blocks.chunk.Chunk

// Replace the entire value
val replaceAll = JsonPatch.root(Op.Set(Json.Null))

// Increment a number at the root
val increment = JsonPatch.root(Op.PrimitiveDelta(PrimitiveOp.NumberDelta(BigDecimal(1))))

// Add a field to a root object
val addField = JsonPatch.root(Op.ObjectEdit(Chunk(ObjectOp.Add("active", Json.Boolean(true)))))

JsonPatch.apply

Creates a patch with a single operation applied at the specified DynamicOptic path. Use this when targeting a nested location within the value:

object JsonPatch {
def apply(path: DynamicOptic, operation: JsonPatch.Op): JsonPatch
}

Paths are built fluently on DynamicOptic.root using .field(name) to navigate object fields and .at(index) to navigate array elements. For instance, to increment age inside a user object:

import zio.blocks.schema.json.{Json, JsonPatch}
import zio.blocks.schema.json.JsonPatch._
import zio.blocks.schema.DynamicOptic

val agePath = DynamicOptic.root.field("user").field("age")
val agePatch = JsonPatch(agePath, Op.PrimitiveDelta(PrimitiveOp.NumberDelta(BigDecimal(1))))
val nested = Json.Object("user" -> Json.Object("name" -> Json.String("Alice"), "age" -> Json.Number(25)))

Applying the patch navigates to the nested age field and increments it:

agePatch.apply(nested)
// res5: Either[SchemaError, Json] = Right(
// Object(
// IndexedSeq(
// (
// "user",
// Object(IndexedSeq(("name", String("Alice")), ("age", Number(26))))
// )
// )
// )
// )

JsonPatch.empty

The empty patch. Applying it to any Json value returns that value unchanged. JsonPatch.empty is the identity element for ++:

object JsonPatch {
val empty: JsonPatch
}

JsonPatch.empty is useful as a neutral starting point when building patches conditionally:

import zio.blocks.schema.json.{Json, JsonPatch}

Both JsonPatch#isEmpty and applying JsonPatch.empty confirm the identity property:

JsonPatch.empty.isEmpty
// res7: Boolean = true
JsonPatch.empty.apply(Json.Number(42))
// res8: Either[SchemaError, Json] = Right(Number(42))
(JsonPatch.empty ++ JsonPatch.empty).isEmpty
// res9: Boolean = true

JsonPatch.fromDynamicPatch

Converts a generic DynamicPatch to a JsonPatch. Returns Left[SchemaError] for operations not representable in JSON:

  • Temporal deltas (InstantDelta, DurationDelta, etc.) — JSON has no native time type
  • Non-string map keys — JSON object keys must always be strings

All numeric delta types (IntDelta, LongDelta, DoubleDelta, etc.) are widened to NumberDelta(BigDecimal):

object JsonPatch {
def fromDynamicPatch(patch: DynamicPatch): Either[SchemaError, JsonPatch]
}

The round-trip through DynamicPatch preserves numeric deltas:

import zio.blocks.schema.json.{Json, JsonPatch}
import zio.blocks.schema.patch.DynamicPatch
import zio.blocks.schema.SchemaError

val original: JsonPatch = JsonPatch.diff(Json.Number(1), Json.Number(2))
val dynPatch: DynamicPatch = original.toDynamicPatch
val back: Either[SchemaError, JsonPatch] = JsonPatch.fromDynamicPatch(dynPatch)

The roundtrip succeeds and the recovered patch equals the original:

back == Right(original)
// res11: Boolean = true

Core Operations

JsonPatch exposes operations for applying patches, composing them, and converting between formats. The three groups of operations are applying, composing, and converting.

Applying Patches

The primary way to use a JsonPatch is to call JsonPatch#apply or the Json#patch extension method, both of which accept an optional PatchMode argument.

apply

Applies this patch to a Json value. Returns Right with the patched value on success, or Left[SchemaError] on failure. The mode parameter controls failure handling — see PatchMode:

case class JsonPatch(ops: Chunk[JsonPatch.JsonPatchOp]) {
def apply(value: Json, mode: PatchMode = PatchMode.Strict): Either[SchemaError, Json]
}

apply is also available via the Json#patch extension method. Both forms produce the same result:

import zio.blocks.schema.json.{Json, JsonPatch}
import zio.blocks.schema.json.JsonPatch._
import zio.blocks.schema.patch.PatchMode
import zio.blocks.chunk.Chunk

val applyJson = Json.Object("score" -> Json.Number(10))
val applyPatch = JsonPatch.root(Op.ObjectEdit(Chunk(
ObjectOp.Modify("score", JsonPatch.root(Op.PrimitiveDelta(PrimitiveOp.NumberDelta(BigDecimal(5)))))
)))

The direct call and the extension method are equivalent:

applyPatch.apply(applyJson)
// res13: Either[SchemaError, Json] = Right(
// Object(IndexedSeq(("score", Number(15))))
// )
applyJson.patch(applyPatch)
// res14: Either[SchemaError, Json] = Right(
// Object(IndexedSeq(("score", Number(15))))
// )
applyJson.patch(applyPatch, PatchMode.Lenient)
// res15: Either[SchemaError, Json] = Right(
// Object(IndexedSeq(("score", Number(15))))
// )

isEmpty

Returns true if this patch contains no operations:

case class JsonPatch(ops: Chunk[JsonPatch.JsonPatchOp]) {
def isEmpty: Boolean
}

A patch computed between two identical values also produces an empty patch:

import zio.blocks.schema.json.{Json, JsonPatch}
JsonPatch.empty.isEmpty
// res17: Boolean = true
JsonPatch.diff(Json.Number(1), Json.Number(1)).isEmpty
// res18: Boolean = true

Composing Patches

++ is the principal operator for building complex patches from smaller, focused ones. The JsonPatch.empty value is the identity element for ++:

case class JsonPatch(ops: Chunk[JsonPatch.JsonPatchOp]) {
def ++(that: JsonPatch): JsonPatch
}

Concatenating two patches applies this first, then that. This allows building a single patch that updates multiple fields independently:

import zio.blocks.schema.json.{Json, JsonPatch}
import zio.blocks.schema.json.JsonPatch._
import zio.blocks.schema.DynamicOptic

val renamePatch = JsonPatch(
DynamicOptic.root.field("name"),
Op.Set(Json.String("Bob"))
)
val incrAgePatch = JsonPatch(
DynamicOptic.root.field("age"),
Op.PrimitiveDelta(PrimitiveOp.NumberDelta(BigDecimal(1)))
)
val combinedPatch = renamePatch ++ incrAgePatch
val personJson = Json.Object("name" -> Json.String("Alice"), "age" -> Json.Number(25))

The combined patch applies both operations in sequence:

combinedPatch.apply(personJson)
// res20: Either[SchemaError, Json] = Right(
// Object(IndexedSeq(("name", String("Bob")), ("age", Number(26))))
// )

Converting

toDynamicPatch converts a JsonPatch to a DynamicPatch. This is always safe — every JSON operation maps to a corresponding dynamic operation. NumberDelta widens to BigDecimalDelta:

case class JsonPatch(ops: Chunk[JsonPatch.JsonPatchOp]) {
def toDynamicPatch: DynamicPatch
}

To convert in the opposite direction, use JsonPatch.fromDynamicPatch — see Creating Patches above:

import zio.blocks.schema.json.{Json, JsonPatch}
import zio.blocks.schema.patch.DynamicPatch

val patch: JsonPatch = JsonPatch.diff(Json.Number(1), Json.Number(5))
val dyn: DynamicPatch = patch.toDynamicPatch

PatchMode

PatchMode controls how JsonPatch#apply reacts when an operation's precondition is not met (e.g., a field is missing, or ObjectOp.Add targets a key that already exists):

ModeBehaviour
PatchMode.Strict (default)Returns Left[SchemaError] on the first failure
PatchMode.LenientSilently skips failing operations; returns Right with partial result
PatchMode.ClobberOverwrites on conflicts; forces through missing-field errors where possible

ObjectOp.Add fails in Strict mode when the key already exists. In Lenient mode the conflicting add is silently skipped; in Clobber mode it overwrites the existing value:

import zio.blocks.schema.json.{Json, JsonPatch}
import zio.blocks.schema.json.JsonPatch._
import zio.blocks.schema.patch.PatchMode
import zio.blocks.chunk.Chunk

val modeJson = Json.Object("a" -> Json.Number(1))
val modePatch = JsonPatch.root(Op.ObjectEdit(Chunk(ObjectOp.Add("a", Json.Number(99)))))

The three modes produce different outcomes for the same conflicting patch:

modeJson.patch(modePatch, PatchMode.Strict)
// res23: Either[SchemaError, Json] = Left(
// SchemaError(
// List(
// ExpectationMismatch(
// source = DynamicOptic(ArraySeq()),
// expectation = "Key 'a' already exists in object"
// )
// )
// )
// )
modeJson.patch(modePatch, PatchMode.Lenient)
// res24: Either[SchemaError, Json] = Right(
// Object(IndexedSeq(("a", Number(1))))
// )
modeJson.patch(modePatch, PatchMode.Clobber)
// res25: Either[SchemaError, Json] = Right(
// Object(IndexedSeq(("a", Number(99))))
// )

Operation Types

A JsonPatch is a sequence of JsonPatchOp values. Each JsonPatchOp pairs a DynamicOptic path with an Op:

final case class JsonPatchOp(path: DynamicOptic, operation: Op)

The full Op hierarchy covers five cases, from full replacement to fine-grained array and object edits:

Op (sealed trait)
├── Op.Set — replace the target value entirely
├── Op.PrimitiveDelta — numeric increment or string edit
│ ├── PrimitiveOp.NumberDelta
│ └── PrimitiveOp.StringEdit
│ ├── StringOp.Insert
│ ├── StringOp.Delete
│ ├── StringOp.Append
│ └── StringOp.Modify
├── Op.ArrayEdit — insert / append / delete / modify array elements
│ ├── ArrayOp.Insert
│ ├── ArrayOp.Append
│ ├── ArrayOp.Delete
│ └── ArrayOp.Modify
├── Op.ObjectEdit — add / remove / modify object fields
│ ├── ObjectOp.Add
│ ├── ObjectOp.Remove
│ └── ObjectOp.Modify
└── Op.Nested — groups a sub-patch under a shared path prefix

Op.Set

Replaces the target value with a new Json value, regardless of the current value. Works on any Json type:

final case class Set(value: Json) extends Op

Op.Set can replace across types — for example, replacing a number with a string, or resetting a nested field to null:

import zio.blocks.schema.json.{Json, JsonPatch}
import zio.blocks.schema.json.JsonPatch._
import zio.blocks.schema.DynamicOptic

val setString = JsonPatch.root(Op.Set(Json.String("replaced")))
val setNull = JsonPatch(DynamicOptic.root.field("status"), Op.Set(Json.Null))
val withStatus = Json.Object("status" -> Json.String("active"), "id" -> Json.Number(1))

Both patches replace their target regardless of its current type:

setString.apply(Json.Number(123))
// res27: Either[SchemaError, Json] = Right(String("replaced"))
setNull.apply(withStatus)
// res28: Either[SchemaError, Json] = Right(
// Object(IndexedSeq(("status", null), ("id", Number(1))))
// )

Op.PrimitiveDelta

Applies a primitive mutation to a scalar value — either a numeric increment (NumberDelta) or a sequence of string edits (StringEdit):

final case class PrimitiveDelta(op: PrimitiveOp) extends Op

PrimitiveOp.NumberDelta

Adds delta to a Json.Number. Use a negative value to subtract. Fails if the target is not a Json.Number:

final case class NumberDelta(delta: BigDecimal) extends PrimitiveOp

Positive deltas increment; negative deltas decrement:

import zio.blocks.schema.json.{Json, JsonPatch}
import zio.blocks.schema.json.JsonPatch._

val inc = JsonPatch.root(Op.PrimitiveDelta(PrimitiveOp.NumberDelta(BigDecimal(5))))
val dec = JsonPatch.root(Op.PrimitiveDelta(PrimitiveOp.NumberDelta(BigDecimal(-3))))
inc.apply(Json.Number(10))
// res30: Either[SchemaError, Json] = Right(Number(15))
dec.apply(Json.Number(10))
// res31: Either[SchemaError, Json] = Right(Number(7))

PrimitiveOp.StringEdit

Applies a sequence of StringOp operations to a Json.String. JsonPatch.diff generates StringEdit automatically when it is more compact than a full Set:

final case class StringEdit(ops: Chunk[StringOp]) extends PrimitiveOp

The StringOp cases:

CaseParametersEffect
StringOp.Insert(index, text)position, textInserts text before character index
StringOp.Delete(index, length)position, countRemoves length characters starting at index
StringOp.Append(text)textAppends text to the end
StringOp.Modify(index, length, text)position, count, textReplaces length characters at index with text

We can insert a prefix before the first character using StringOp.Insert:

import zio.blocks.schema.json.{Json, JsonPatch}
import zio.blocks.schema.json.JsonPatch._
import zio.blocks.chunk.Chunk

val insertPatch = JsonPatch.root(
Op.PrimitiveDelta(PrimitiveOp.StringEdit(Chunk(StringOp.Insert(0, "Hello, "))))
)
insertPatch.apply(Json.String("world"))
// res33: Either[SchemaError, Json] = Right(String("Hello, world"))
tip

For most use cases, let JsonPatch.diff generate StringEdit automatically. The diff algorithm uses an LCS (Longest Common Subsequence) comparison and only emits StringEdit when it produces fewer bytes than a plain Set.

Op.ArrayEdit

Applies a sequence of ArrayOp operations to a Json.Array. Operations are applied in order, and each one sees the result of the previous:

final case class ArrayEdit(ops: Chunk[ArrayOp]) extends Op

The ArrayOp cases:

CaseParametersEffect
ArrayOp.Insert(index, values)position, elementsInserts values before index
ArrayOp.Append(values)elementsAppends values to the end
ArrayOp.Delete(index, count)position, countRemoves count elements starting at index
ArrayOp.Modify(index, op)position, opApplies op to the element at index

Multiple ArrayOps in a single ArrayEdit can be combined — here we transform [1, 2, 3] into [0, 1, 2, 4] in one pass:

import zio.blocks.schema.json.{Json, JsonPatch}
import zio.blocks.schema.json.JsonPatch._
import zio.blocks.chunk.Chunk

val arrayPatch = JsonPatch.root(Op.ArrayEdit(Chunk(
ArrayOp.Insert(0, Chunk(Json.Number(0))),
ArrayOp.Delete(3, 1),
ArrayOp.Append(Chunk(Json.Number(4)))
)))
val originalArr = Json.Array(Json.Number(1), Json.Number(2), Json.Number(3))
arrayPatch.apply(originalArr)
// res35: Either[SchemaError, Json] = Right(
// Array(IndexedSeq(Number(0), Number(1), Number(2), Number(4)))
// )
note

Array indices in ArrayOp.Delete and ArrayOp.Modify refer to the state of the array after all preceding ops in the same ArrayEdit have been applied.

Op.ObjectEdit

Applies a sequence of ObjectOp operations to a Json.Object. Operations are applied in order:

final case class ObjectEdit(ops: Chunk[ObjectOp]) extends Op

The ObjectOp cases:

CaseParametersEffect
ObjectOp.Add(key, value)field name, valueAdds a new field; fails in Strict mode if key exists
ObjectOp.Remove(key)field nameRemoves an existing field
ObjectOp.Modify(key, patch)field name, sub-patchApplies patch recursively to the field value

A single ObjectEdit can add, remove, and modify fields in one operation:

import zio.blocks.schema.json.{Json, JsonPatch}
import zio.blocks.schema.json.JsonPatch._
import zio.blocks.chunk.Chunk

val originalObj = Json.Object(
"name" -> Json.String("Alice"),
"age" -> Json.Number(25),
"city" -> Json.String("NYC")
)

val objPatch = JsonPatch.root(Op.ObjectEdit(Chunk(
ObjectOp.Add("email", Json.String("alice@example.com")),
ObjectOp.Remove("city"),
ObjectOp.Modify("age", JsonPatch.root(Op.PrimitiveDelta(PrimitiveOp.NumberDelta(BigDecimal(1)))))
)))
objPatch.apply(originalObj)
// res37: Either[SchemaError, Json] = Right(
// Object(
// IndexedSeq(
// ("name", String("Alice")),
// ("age", Number(26)),
// ("email", String("alice@example.com"))
// )
// )
// )

Op.Nested

Groups a sub-patch under a shared path prefix. JsonPatch.diff emits Nested automatically when multiple operations share a common navigation path — this avoids repeating the full path in each JsonPatchOp:

final case class Nested(patch: JsonPatch) extends Op

You rarely need to construct Nested manually; it is primarily an internal optimization used by the diff algorithm.

Diffing Algorithm

JsonPatch.diff (and its alias Json#diff) delegate to JsonDiffer.diff, which selects the most compact representation for each type of change:

Value typeChangeStrategy
AnyNo changeNo operation emitted
Json.NumberValue changedNumberDelta — stores the numeric difference
Json.StringValue changedStringEdit via LCS if smaller; otherwise Set
Json.ArrayElements changedArrayEdit with LCS-aligned Insert/Delete/Append/Modify
Json.ObjectFields changedObjectEdit with recursive per-field diff
AnyType changedSet — full replacement
tip

JsonPatch.diff followed by JsonPatch#apply is always a lossless roundtrip: for any source and target, JsonPatch.diff(source, target).apply(source) == Right(target).

JsonPatch vs RFC 6902 JSON Patch

ZIO Blocks' JsonPatch is not an implementation of RFC 6902. The two share the same motivation but differ in design:

ZIO Blocks JsonPatchRFC 6902 JSON Patch
OperationsTyped ADT (Op.Set, Op.ArrayEdit, …)String-tagged JSON objects ("op": "replace")
PathsDynamicOptic (typed, composable)JSON Pointer strings ("/a/b/0")
Number changesNumberDelta (stores diff)replace (stores full new value)
String changesLCS-based StringEditreplace only
Array changesLCS-aligned insert/deleteadd, remove, replace at absolute indices
SerializationVia ZIO Blocks Schema in any formatAlways JSON
Composition++ operatorArray concatenation

Use JsonPatch when working within ZIO Blocks. For interoperability with RFC 6902 tooling, convert the patch to JSON using the built-in Schema instances and reformat as needed.

Advanced Usage

JsonPatch's composability and first-class serializability unlock patterns beyond simple point-in-time updates.

Building a Change Log

Because JsonPatch is a pure value with a Schema, we can serialize every change and replay or audit it later:

import zio.blocks.schema.json.{Json, JsonPatch}

// Every mutation is a patch — store it instead of overwriting
val v0 = Json.Object("count" -> Json.Number(0))
val v1 = Json.Object("count" -> Json.Number(1))
val v2 = Json.Object("count" -> Json.Number(2))

val log: List[JsonPatch] = List(
JsonPatch.diff(v0, v1),
JsonPatch.diff(v1, v2)
)

// Replay: reconstruct any historical state
val replay = log.foldLeft(v0: Json)((state, patch) => patch.apply(state).getOrElse(state))
assert(replay == v2)

Composing Targeted Sub-Patches

We can build a single patch that updates multiple nested fields by combining focused per-field patches with ++:

import zio.blocks.schema.json.{Json, JsonPatch}
import zio.blocks.schema.json.JsonPatch._
import zio.blocks.schema.DynamicOptic

def setField(field: String, value: Json): JsonPatch =
JsonPatch(DynamicOptic.root.field(field), Op.Set(value))

val fieldPatch =
setField("status", Json.String("active")) ++
setField("updatedAt", Json.String("2025-01-01"))

val doc = Json.Object("status" -> Json.String("draft"), "id" -> Json.Number(42))

Applying the composed patch updates both fields in one step:

fieldPatch.apply(doc)
// res40: Either[SchemaError, Json] = Left(
// SchemaError(
// List(
// MissingField(source = DynamicOptic(ArraySeq()), fieldName = "updatedAt")
// )
// )
// )

Integration

JsonPatch integrates with Json, DynamicPatch, and the ZIO Blocks serialization system. Each integration point is covered below.

With Json

Json exposes two extension methods as entry points into JsonPatch:

import zio.blocks.schema.json.{Json, JsonPatch}
import zio.blocks.schema.patch.PatchMode

val source = Json.Object("x" -> Json.Number(1))
val target = Json.Object("x" -> Json.Number(2))

val patch: JsonPatch = source.diff(target) // compute patch
val result = source.patch(patch) // apply (Strict)
val lenient = source.patch(patch, PatchMode.Lenient)

See Json for the complete Json API.

With DynamicPatch

JsonPatch and DynamicPatch are bidirectionally convertible. This is useful when patches originate from the typed Patch[S] system and need to be applied to raw JSON:

import zio.blocks.schema.json.{Json, JsonPatch}
import zio.blocks.schema.patch.DynamicPatch
import zio.blocks.schema.SchemaError

val jsonPatch: JsonPatch = JsonPatch.diff(Json.Number(1), Json.Number(3))

// JsonPatch → DynamicPatch (always succeeds)
val dynPatch: DynamicPatch = jsonPatch.toDynamicPatch

// DynamicPatch → JsonPatch (may fail for temporal ops or non-string keys)
val back: Either[SchemaError, JsonPatch] = JsonPatch.fromDynamicPatch(dynPatch)

See Patching for the typed Patch[S] API.

Serialization

JsonPatch ships with Schema instances for all nested operation types, enabling round-trip serialization via any ZIO Blocks codec:

import zio.blocks.schema.json.JsonPatch
import zio.blocks.schema.Schema

val schema: Schema[JsonPatch] = implicitly[Schema[JsonPatch]]

See Codec & Format for how to derive and use codecs.

Examples

Runnable examples are in schema-examples/src/main/scala/jsonpatch/:

FileTopic
JsonPatchDiffAndApplyExample.scalaJsonPatch.diff, Json#diff, Json#patch, roundtrip guarantee
JsonPatchManualBuildExample.scalaJsonPatch.root, path-based patches, JsonPatch.empty
JsonPatchOperationsExample.scalaAll Op types — Set, NumberDelta, StringEdit, ArrayEdit, ObjectEdit
JsonPatchCompositionExample.scala++, PatchMode, toDynamicPatch, fromDynamicPatch
CompleteJsonPatchExample.scalaCollaborative document editor with a full patch log, replay, and sync

Run any example with:

sbt "schema-examples/runMain jsonpatch.CompleteJsonPatchExample"