@zio.dev/zio-blocks 0.0.27 → 0.0.28
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.
- package/guides/query-dsl-extending.md +1 -1
- package/guides/query-dsl-fluent-builder.md +1 -1
- package/guides/query-dsl-reified-optics.md +1 -1
- package/guides/query-dsl-sql.md +1 -1
- package/guides/zio-schema-migration.md +6 -6
- package/index.md +18 -12
- package/package.json +1 -1
- package/reference/allows.md +1059 -34
- package/reference/binding-resolver.md +469 -0
- package/reference/binding.md +1 -1
- package/reference/codec.md +7 -7
- package/reference/docs.md +1 -1
- package/reference/dynamic-optic.md +5 -0
- package/reference/dynamic-schema.md +602 -0
- package/reference/dynamic-value.md +5 -0
- package/reference/json-patch.md +803 -0
- package/reference/json.md +1 -1
- package/reference/media-type.md +2 -2
- package/reference/patch.md +4 -0
- package/reference/schema-evolution/as.md +587 -0
- package/reference/schema-evolution/index.md +50 -0
- package/reference/schema-evolution/into.md +1027 -0
- package/reference/schema-expr.md +2 -2
- package/reference/structural-types.md +369 -0
- package/reference/type-class-derivation.md +1 -1
- package/reference/xml.md +606 -45
- package/scope.md +91 -9
- package/sidebars.js +19 -1
- package/reference/schema-evolution.md +0 -540
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: json-patch
|
|
3
|
+
title: "JsonPatch"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`JsonPatch` is an **untyped, composable patch** for [`Json`](./json.md) 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.
|
|
7
|
+
|
|
8
|
+
`JsonPatch`:
|
|
9
|
+
- is a pure value — applying it never mutates the input
|
|
10
|
+
- is composable via `++`, sequencing two patches one after another
|
|
11
|
+
- supports three failure-handling modes: `Strict`, `Lenient`, and `Clobber`
|
|
12
|
+
- carries its own `Schema` instances for full serialization support
|
|
13
|
+
- converts bidirectionally to/from `DynamicPatch` for use in generic patching pipelines
|
|
14
|
+
|
|
15
|
+
The `JsonPatch` type wraps a sequence of operations:
|
|
16
|
+
|
|
17
|
+
```scala
|
|
18
|
+
final case class JsonPatch(ops: Chunk[JsonPatch.JsonPatchOp])
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Motivation
|
|
22
|
+
|
|
23
|
+
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:
|
|
24
|
+
|
|
25
|
+
- **Transmitted efficiently** — send only what changed, not the entire document
|
|
26
|
+
- **Stored for audit logs** — record every change for compliance, debugging, or undo
|
|
27
|
+
- **Composed** — merge multiple changes into a single atomic patch
|
|
28
|
+
- **Serialized** — persist patches to disk or a message queue and replay them later
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
Source JSON Target JSON
|
|
32
|
+
┌─────────────────────┐ ┌─────────────────────┐
|
|
33
|
+
│ { "name": "Alice", │ │ { "name": "Alice", │
|
|
34
|
+
│ "age": 25, │──diff──│ "age": 26, │
|
|
35
|
+
│ "city": "NYC" } │ │ "city": "NYC" } │
|
|
36
|
+
└─────────────────────┘ └─────────────────────┘
|
|
37
|
+
│
|
|
38
|
+
▼
|
|
39
|
+
JsonPatch {
|
|
40
|
+
ObjectEdit(
|
|
41
|
+
Modify("age", NumberDelta(1))
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
│
|
|
45
|
+
▼ apply
|
|
46
|
+
┌─────────────────────┐
|
|
47
|
+
│ { "name": "Alice", │
|
|
48
|
+
│ "age": 26, │
|
|
49
|
+
│ "city": "NYC" } │
|
|
50
|
+
└─────────────────────┘
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
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`:
|
|
54
|
+
|
|
55
|
+
```scala
|
|
56
|
+
import zio.blocks.schema.json.{Json, JsonPatch}
|
|
57
|
+
|
|
58
|
+
val source = Json.Object("name" -> Json.String("Alice"), "age" -> Json.Number(25))
|
|
59
|
+
val target = Json.Object("name" -> Json.String("Alice"), "age" -> Json.Number(26))
|
|
60
|
+
|
|
61
|
+
val patch: JsonPatch = JsonPatch.diff(source, target)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Applying the patch to `source` always yields `Right(target)`:
|
|
65
|
+
|
|
66
|
+
```scala
|
|
67
|
+
patch.apply(source) == Right(target)
|
|
68
|
+
// res1: Boolean = true
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Creating Patches
|
|
72
|
+
|
|
73
|
+
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`.
|
|
74
|
+
|
|
75
|
+
### `JsonPatch.diff`
|
|
76
|
+
|
|
77
|
+
Computes the minimal `JsonPatch` that transforms `source` into `target`. Uses a smart diff strategy per value type — see the [Diffing Algorithm](#diffing-algorithm) section for details:
|
|
78
|
+
|
|
79
|
+
```scala
|
|
80
|
+
object JsonPatch {
|
|
81
|
+
def diff(source: Json, target: Json): JsonPatch
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`JsonPatch.diff` is also available as the `Json#diff` extension method:
|
|
86
|
+
|
|
87
|
+
```scala
|
|
88
|
+
import zio.blocks.schema.json.{Json, JsonPatch}
|
|
89
|
+
|
|
90
|
+
// Via companion object
|
|
91
|
+
val p1 = JsonPatch.diff(Json.Number(10), Json.Number(15))
|
|
92
|
+
|
|
93
|
+
// Via extension method on Json
|
|
94
|
+
val p2 = Json.Number(10).diff(Json.Number(15))
|
|
95
|
+
|
|
96
|
+
// Nested object diff produces minimal ObjectEdit
|
|
97
|
+
val p3 = JsonPatch.diff(
|
|
98
|
+
Json.Object("a" -> Json.Number(1), "b" -> Json.Number(2)),
|
|
99
|
+
Json.Object("a" -> Json.Number(1), "b" -> Json.Number(9))
|
|
100
|
+
)
|
|
101
|
+
// p3 only touches "b", leaves "a" unchanged
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### `JsonPatch.root`
|
|
105
|
+
|
|
106
|
+
Creates a patch with a single operation applied at the root of the value:
|
|
107
|
+
|
|
108
|
+
```scala
|
|
109
|
+
object JsonPatch {
|
|
110
|
+
def root(operation: JsonPatch.Op): JsonPatch
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
For example, we can replace the root entirely, increment a number, or add a field to a root object:
|
|
115
|
+
|
|
116
|
+
```scala
|
|
117
|
+
import zio.blocks.schema.json.{Json, JsonPatch}
|
|
118
|
+
import zio.blocks.schema.json.JsonPatch._
|
|
119
|
+
import zio.blocks.chunk.Chunk
|
|
120
|
+
|
|
121
|
+
// Replace the entire value
|
|
122
|
+
val replaceAll = JsonPatch.root(Op.Set(Json.Null))
|
|
123
|
+
|
|
124
|
+
// Increment a number at the root
|
|
125
|
+
val increment = JsonPatch.root(Op.PrimitiveDelta(PrimitiveOp.NumberDelta(BigDecimal(1))))
|
|
126
|
+
|
|
127
|
+
// Add a field to a root object
|
|
128
|
+
val addField = JsonPatch.root(Op.ObjectEdit(Chunk(ObjectOp.Add("active", Json.Boolean(true)))))
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### `JsonPatch.apply`
|
|
132
|
+
|
|
133
|
+
Creates a patch with a single operation applied at the specified `DynamicOptic` path. Use this when targeting a nested location within the value:
|
|
134
|
+
|
|
135
|
+
```scala
|
|
136
|
+
object JsonPatch {
|
|
137
|
+
def apply(path: DynamicOptic, operation: JsonPatch.Op): JsonPatch
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
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:
|
|
142
|
+
|
|
143
|
+
```scala
|
|
144
|
+
import zio.blocks.schema.json.{Json, JsonPatch}
|
|
145
|
+
import zio.blocks.schema.json.JsonPatch._
|
|
146
|
+
import zio.blocks.schema.DynamicOptic
|
|
147
|
+
|
|
148
|
+
val agePath = DynamicOptic.root.field("user").field("age")
|
|
149
|
+
val agePatch = JsonPatch(agePath, Op.PrimitiveDelta(PrimitiveOp.NumberDelta(BigDecimal(1))))
|
|
150
|
+
val nested = Json.Object("user" -> Json.Object("name" -> Json.String("Alice"), "age" -> Json.Number(25)))
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Applying the patch navigates to the nested `age` field and increments it:
|
|
154
|
+
|
|
155
|
+
```scala
|
|
156
|
+
agePatch.apply(nested)
|
|
157
|
+
// res5: Either[SchemaError, Json] = Right(
|
|
158
|
+
// Object(
|
|
159
|
+
// IndexedSeq(
|
|
160
|
+
// (
|
|
161
|
+
// "user",
|
|
162
|
+
// Object(IndexedSeq(("name", String("Alice")), ("age", Number(26))))
|
|
163
|
+
// )
|
|
164
|
+
// )
|
|
165
|
+
// )
|
|
166
|
+
// )
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### `JsonPatch.empty`
|
|
170
|
+
|
|
171
|
+
The empty patch. Applying it to any `Json` value returns that value unchanged. `JsonPatch.empty` is the identity element for `++`:
|
|
172
|
+
|
|
173
|
+
```scala
|
|
174
|
+
object JsonPatch {
|
|
175
|
+
val empty: JsonPatch
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`JsonPatch.empty` is useful as a neutral starting point when building patches conditionally:
|
|
180
|
+
|
|
181
|
+
```scala
|
|
182
|
+
import zio.blocks.schema.json.{Json, JsonPatch}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Both `JsonPatch#isEmpty` and applying `JsonPatch.empty` confirm the identity property:
|
|
186
|
+
|
|
187
|
+
```scala
|
|
188
|
+
JsonPatch.empty.isEmpty
|
|
189
|
+
// res7: Boolean = true
|
|
190
|
+
JsonPatch.empty.apply(Json.Number(42))
|
|
191
|
+
// res8: Either[SchemaError, Json] = Right(Number(42))
|
|
192
|
+
(JsonPatch.empty ++ JsonPatch.empty).isEmpty
|
|
193
|
+
// res9: Boolean = true
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### `JsonPatch.fromDynamicPatch`
|
|
197
|
+
|
|
198
|
+
Converts a generic `DynamicPatch` to a `JsonPatch`. Returns `Left[SchemaError]` for operations not representable in JSON:
|
|
199
|
+
- Temporal deltas (`InstantDelta`, `DurationDelta`, etc.) — JSON has no native time type
|
|
200
|
+
- Non-string map keys — JSON object keys must always be strings
|
|
201
|
+
|
|
202
|
+
All numeric delta types (`IntDelta`, `LongDelta`, `DoubleDelta`, etc.) are widened to `NumberDelta(BigDecimal)`:
|
|
203
|
+
|
|
204
|
+
```scala
|
|
205
|
+
object JsonPatch {
|
|
206
|
+
def fromDynamicPatch(patch: DynamicPatch): Either[SchemaError, JsonPatch]
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
The round-trip through `DynamicPatch` preserves numeric deltas:
|
|
211
|
+
|
|
212
|
+
```scala
|
|
213
|
+
import zio.blocks.schema.json.{Json, JsonPatch}
|
|
214
|
+
import zio.blocks.schema.patch.DynamicPatch
|
|
215
|
+
import zio.blocks.schema.SchemaError
|
|
216
|
+
|
|
217
|
+
val original: JsonPatch = JsonPatch.diff(Json.Number(1), Json.Number(2))
|
|
218
|
+
val dynPatch: DynamicPatch = original.toDynamicPatch
|
|
219
|
+
val back: Either[SchemaError, JsonPatch] = JsonPatch.fromDynamicPatch(dynPatch)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The roundtrip succeeds and the recovered patch equals the original:
|
|
223
|
+
|
|
224
|
+
```scala
|
|
225
|
+
back == Right(original)
|
|
226
|
+
// res11: Boolean = true
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Core Operations
|
|
230
|
+
|
|
231
|
+
`JsonPatch` exposes operations for applying patches, composing them, and converting between formats. The three groups of operations are applying, composing, and converting.
|
|
232
|
+
|
|
233
|
+
### Applying Patches
|
|
234
|
+
|
|
235
|
+
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.
|
|
236
|
+
|
|
237
|
+
#### `apply`
|
|
238
|
+
|
|
239
|
+
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`](#patchmode):
|
|
240
|
+
|
|
241
|
+
```scala
|
|
242
|
+
case class JsonPatch(ops: Chunk[JsonPatch.JsonPatchOp]) {
|
|
243
|
+
def apply(value: Json, mode: PatchMode = PatchMode.Strict): Either[SchemaError, Json]
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`apply` is also available via the `Json#patch` extension method. Both forms produce the same result:
|
|
248
|
+
|
|
249
|
+
```scala
|
|
250
|
+
import zio.blocks.schema.json.{Json, JsonPatch}
|
|
251
|
+
import zio.blocks.schema.json.JsonPatch._
|
|
252
|
+
import zio.blocks.schema.patch.PatchMode
|
|
253
|
+
import zio.blocks.chunk.Chunk
|
|
254
|
+
|
|
255
|
+
val applyJson = Json.Object("score" -> Json.Number(10))
|
|
256
|
+
val applyPatch = JsonPatch.root(Op.ObjectEdit(Chunk(
|
|
257
|
+
ObjectOp.Modify("score", JsonPatch.root(Op.PrimitiveDelta(PrimitiveOp.NumberDelta(BigDecimal(5)))))
|
|
258
|
+
)))
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
The direct call and the extension method are equivalent:
|
|
262
|
+
|
|
263
|
+
```scala
|
|
264
|
+
applyPatch.apply(applyJson)
|
|
265
|
+
// res13: Either[SchemaError, Json] = Right(
|
|
266
|
+
// Object(IndexedSeq(("score", Number(15))))
|
|
267
|
+
// )
|
|
268
|
+
applyJson.patch(applyPatch)
|
|
269
|
+
// res14: Either[SchemaError, Json] = Right(
|
|
270
|
+
// Object(IndexedSeq(("score", Number(15))))
|
|
271
|
+
// )
|
|
272
|
+
applyJson.patch(applyPatch, PatchMode.Lenient)
|
|
273
|
+
// res15: Either[SchemaError, Json] = Right(
|
|
274
|
+
// Object(IndexedSeq(("score", Number(15))))
|
|
275
|
+
// )
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
#### `isEmpty`
|
|
279
|
+
|
|
280
|
+
Returns `true` if this patch contains no operations:
|
|
281
|
+
|
|
282
|
+
```scala
|
|
283
|
+
case class JsonPatch(ops: Chunk[JsonPatch.JsonPatchOp]) {
|
|
284
|
+
def isEmpty: Boolean
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
A patch computed between two identical values also produces an empty patch:
|
|
289
|
+
|
|
290
|
+
```scala
|
|
291
|
+
import zio.blocks.schema.json.{Json, JsonPatch}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
```scala
|
|
295
|
+
JsonPatch.empty.isEmpty
|
|
296
|
+
// res17: Boolean = true
|
|
297
|
+
JsonPatch.diff(Json.Number(1), Json.Number(1)).isEmpty
|
|
298
|
+
// res18: Boolean = true
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Composing Patches
|
|
302
|
+
|
|
303
|
+
`++` is the principal operator for building complex patches from smaller, focused ones. The `JsonPatch.empty` value is the identity element for `++`:
|
|
304
|
+
|
|
305
|
+
```scala
|
|
306
|
+
case class JsonPatch(ops: Chunk[JsonPatch.JsonPatchOp]) {
|
|
307
|
+
def ++(that: JsonPatch): JsonPatch
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Concatenating two patches applies `this` first, then `that`. This allows building a single patch that updates multiple fields independently:
|
|
312
|
+
|
|
313
|
+
```scala
|
|
314
|
+
import zio.blocks.schema.json.{Json, JsonPatch}
|
|
315
|
+
import zio.blocks.schema.json.JsonPatch._
|
|
316
|
+
import zio.blocks.schema.DynamicOptic
|
|
317
|
+
|
|
318
|
+
val renamePatch = JsonPatch(
|
|
319
|
+
DynamicOptic.root.field("name"),
|
|
320
|
+
Op.Set(Json.String("Bob"))
|
|
321
|
+
)
|
|
322
|
+
val incrAgePatch = JsonPatch(
|
|
323
|
+
DynamicOptic.root.field("age"),
|
|
324
|
+
Op.PrimitiveDelta(PrimitiveOp.NumberDelta(BigDecimal(1)))
|
|
325
|
+
)
|
|
326
|
+
val combinedPatch = renamePatch ++ incrAgePatch
|
|
327
|
+
val personJson = Json.Object("name" -> Json.String("Alice"), "age" -> Json.Number(25))
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
The combined patch applies both operations in sequence:
|
|
331
|
+
|
|
332
|
+
```scala
|
|
333
|
+
combinedPatch.apply(personJson)
|
|
334
|
+
// res20: Either[SchemaError, Json] = Right(
|
|
335
|
+
// Object(IndexedSeq(("name", String("Bob")), ("age", Number(26))))
|
|
336
|
+
// )
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Converting
|
|
340
|
+
|
|
341
|
+
`toDynamicPatch` converts a `JsonPatch` to a [`DynamicPatch`](./patch.md). This is always safe — every JSON operation maps to a corresponding dynamic operation. `NumberDelta` widens to `BigDecimalDelta`:
|
|
342
|
+
|
|
343
|
+
```scala
|
|
344
|
+
case class JsonPatch(ops: Chunk[JsonPatch.JsonPatchOp]) {
|
|
345
|
+
def toDynamicPatch: DynamicPatch
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
To convert in the opposite direction, use `JsonPatch.fromDynamicPatch` — see [Creating Patches](#creating-patches) above:
|
|
350
|
+
|
|
351
|
+
```scala
|
|
352
|
+
import zio.blocks.schema.json.{Json, JsonPatch}
|
|
353
|
+
import zio.blocks.schema.patch.DynamicPatch
|
|
354
|
+
|
|
355
|
+
val patch: JsonPatch = JsonPatch.diff(Json.Number(1), Json.Number(5))
|
|
356
|
+
val dyn: DynamicPatch = patch.toDynamicPatch
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
## PatchMode
|
|
360
|
+
|
|
361
|
+
`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):
|
|
362
|
+
|
|
363
|
+
| Mode | Behaviour |
|
|
364
|
+
|------|-----------|
|
|
365
|
+
| `PatchMode.Strict` (default) | Returns `Left[SchemaError]` on the first failure |
|
|
366
|
+
| `PatchMode.Lenient` | Silently skips failing operations; returns `Right` with partial result |
|
|
367
|
+
| `PatchMode.Clobber` | Overwrites on conflicts; forces through missing-field errors where possible |
|
|
368
|
+
|
|
369
|
+
`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:
|
|
370
|
+
|
|
371
|
+
```scala
|
|
372
|
+
import zio.blocks.schema.json.{Json, JsonPatch}
|
|
373
|
+
import zio.blocks.schema.json.JsonPatch._
|
|
374
|
+
import zio.blocks.schema.patch.PatchMode
|
|
375
|
+
import zio.blocks.chunk.Chunk
|
|
376
|
+
|
|
377
|
+
val modeJson = Json.Object("a" -> Json.Number(1))
|
|
378
|
+
val modePatch = JsonPatch.root(Op.ObjectEdit(Chunk(ObjectOp.Add("a", Json.Number(99)))))
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
The three modes produce different outcomes for the same conflicting patch:
|
|
382
|
+
|
|
383
|
+
```scala
|
|
384
|
+
modeJson.patch(modePatch, PatchMode.Strict)
|
|
385
|
+
// res23: Either[SchemaError, Json] = Left(
|
|
386
|
+
// SchemaError(
|
|
387
|
+
// List(
|
|
388
|
+
// ExpectationMismatch(
|
|
389
|
+
// source = DynamicOptic(ArraySeq()),
|
|
390
|
+
// expectation = "Key 'a' already exists in object"
|
|
391
|
+
// )
|
|
392
|
+
// )
|
|
393
|
+
// )
|
|
394
|
+
// )
|
|
395
|
+
modeJson.patch(modePatch, PatchMode.Lenient)
|
|
396
|
+
// res24: Either[SchemaError, Json] = Right(
|
|
397
|
+
// Object(IndexedSeq(("a", Number(1))))
|
|
398
|
+
// )
|
|
399
|
+
modeJson.patch(modePatch, PatchMode.Clobber)
|
|
400
|
+
// res25: Either[SchemaError, Json] = Right(
|
|
401
|
+
// Object(IndexedSeq(("a", Number(99))))
|
|
402
|
+
// )
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
## Operation Types
|
|
406
|
+
|
|
407
|
+
A `JsonPatch` is a sequence of `JsonPatchOp` values. Each `JsonPatchOp` pairs a `DynamicOptic` path with an `Op`:
|
|
408
|
+
|
|
409
|
+
```scala
|
|
410
|
+
final case class JsonPatchOp(path: DynamicOptic, operation: Op)
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
The full `Op` hierarchy covers five cases, from full replacement to fine-grained array and object edits:
|
|
414
|
+
|
|
415
|
+
```
|
|
416
|
+
Op (sealed trait)
|
|
417
|
+
├── Op.Set — replace the target value entirely
|
|
418
|
+
├── Op.PrimitiveDelta — numeric increment or string edit
|
|
419
|
+
│ ├── PrimitiveOp.NumberDelta
|
|
420
|
+
│ └── PrimitiveOp.StringEdit
|
|
421
|
+
│ ├── StringOp.Insert
|
|
422
|
+
│ ├── StringOp.Delete
|
|
423
|
+
│ ├── StringOp.Append
|
|
424
|
+
│ └── StringOp.Modify
|
|
425
|
+
├── Op.ArrayEdit — insert / append / delete / modify array elements
|
|
426
|
+
│ ├── ArrayOp.Insert
|
|
427
|
+
│ ├── ArrayOp.Append
|
|
428
|
+
│ ├── ArrayOp.Delete
|
|
429
|
+
│ └── ArrayOp.Modify
|
|
430
|
+
├── Op.ObjectEdit — add / remove / modify object fields
|
|
431
|
+
│ ├── ObjectOp.Add
|
|
432
|
+
│ ├── ObjectOp.Remove
|
|
433
|
+
│ └── ObjectOp.Modify
|
|
434
|
+
└── Op.Nested — groups a sub-patch under a shared path prefix
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### `Op.Set`
|
|
438
|
+
|
|
439
|
+
Replaces the target value with a new `Json` value, regardless of the current value. Works on any `Json` type:
|
|
440
|
+
|
|
441
|
+
```scala
|
|
442
|
+
final case class Set(value: Json) extends Op
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
`Op.Set` can replace across types — for example, replacing a number with a string, or resetting a nested field to `null`:
|
|
446
|
+
|
|
447
|
+
```scala
|
|
448
|
+
import zio.blocks.schema.json.{Json, JsonPatch}
|
|
449
|
+
import zio.blocks.schema.json.JsonPatch._
|
|
450
|
+
import zio.blocks.schema.DynamicOptic
|
|
451
|
+
|
|
452
|
+
val setString = JsonPatch.root(Op.Set(Json.String("replaced")))
|
|
453
|
+
val setNull = JsonPatch(DynamicOptic.root.field("status"), Op.Set(Json.Null))
|
|
454
|
+
val withStatus = Json.Object("status" -> Json.String("active"), "id" -> Json.Number(1))
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Both patches replace their target regardless of its current type:
|
|
458
|
+
|
|
459
|
+
```scala
|
|
460
|
+
setString.apply(Json.Number(123))
|
|
461
|
+
// res27: Either[SchemaError, Json] = Right(String("replaced"))
|
|
462
|
+
setNull.apply(withStatus)
|
|
463
|
+
// res28: Either[SchemaError, Json] = Right(
|
|
464
|
+
// Object(IndexedSeq(("status", null), ("id", Number(1))))
|
|
465
|
+
// )
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### `Op.PrimitiveDelta`
|
|
469
|
+
|
|
470
|
+
Applies a primitive mutation to a scalar value — either a numeric increment (`NumberDelta`) or a sequence of string edits (`StringEdit`):
|
|
471
|
+
|
|
472
|
+
```scala
|
|
473
|
+
final case class PrimitiveDelta(op: PrimitiveOp) extends Op
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
#### `PrimitiveOp.NumberDelta`
|
|
477
|
+
|
|
478
|
+
Adds `delta` to a `Json.Number`. Use a negative value to subtract. Fails if the target is not a `Json.Number`:
|
|
479
|
+
|
|
480
|
+
```scala
|
|
481
|
+
final case class NumberDelta(delta: BigDecimal) extends PrimitiveOp
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
Positive deltas increment; negative deltas decrement:
|
|
485
|
+
|
|
486
|
+
```scala
|
|
487
|
+
import zio.blocks.schema.json.{Json, JsonPatch}
|
|
488
|
+
import zio.blocks.schema.json.JsonPatch._
|
|
489
|
+
|
|
490
|
+
val inc = JsonPatch.root(Op.PrimitiveDelta(PrimitiveOp.NumberDelta(BigDecimal(5))))
|
|
491
|
+
val dec = JsonPatch.root(Op.PrimitiveDelta(PrimitiveOp.NumberDelta(BigDecimal(-3))))
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
```scala
|
|
495
|
+
inc.apply(Json.Number(10))
|
|
496
|
+
// res30: Either[SchemaError, Json] = Right(Number(15))
|
|
497
|
+
dec.apply(Json.Number(10))
|
|
498
|
+
// res31: Either[SchemaError, Json] = Right(Number(7))
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
#### `PrimitiveOp.StringEdit`
|
|
502
|
+
|
|
503
|
+
Applies a sequence of `StringOp` operations to a `Json.String`. `JsonPatch.diff` generates `StringEdit` automatically when it is more compact than a full `Set`:
|
|
504
|
+
|
|
505
|
+
```scala
|
|
506
|
+
final case class StringEdit(ops: Chunk[StringOp]) extends PrimitiveOp
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
The `StringOp` cases:
|
|
510
|
+
|
|
511
|
+
| Case | Parameters | Effect |
|
|
512
|
+
|------|-----------|--------|
|
|
513
|
+
| `StringOp.Insert(index, text)` | position, text | Inserts `text` before character `index` |
|
|
514
|
+
| `StringOp.Delete(index, length)` | position, count | Removes `length` characters starting at `index` |
|
|
515
|
+
| `StringOp.Append(text)` | text | Appends `text` to the end |
|
|
516
|
+
| `StringOp.Modify(index, length, text)` | position, count, text | Replaces `length` characters at `index` with `text` |
|
|
517
|
+
|
|
518
|
+
We can insert a prefix before the first character using `StringOp.Insert`:
|
|
519
|
+
|
|
520
|
+
```scala
|
|
521
|
+
import zio.blocks.schema.json.{Json, JsonPatch}
|
|
522
|
+
import zio.blocks.schema.json.JsonPatch._
|
|
523
|
+
import zio.blocks.chunk.Chunk
|
|
524
|
+
|
|
525
|
+
val insertPatch = JsonPatch.root(
|
|
526
|
+
Op.PrimitiveDelta(PrimitiveOp.StringEdit(Chunk(StringOp.Insert(0, "Hello, "))))
|
|
527
|
+
)
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
```scala
|
|
531
|
+
insertPatch.apply(Json.String("world"))
|
|
532
|
+
// res33: Either[SchemaError, Json] = Right(String("Hello, world"))
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
:::tip
|
|
536
|
+
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`.
|
|
537
|
+
:::
|
|
538
|
+
|
|
539
|
+
### `Op.ArrayEdit`
|
|
540
|
+
|
|
541
|
+
Applies a sequence of `ArrayOp` operations to a `Json.Array`. Operations are applied in order, and each one sees the result of the previous:
|
|
542
|
+
|
|
543
|
+
```scala
|
|
544
|
+
final case class ArrayEdit(ops: Chunk[ArrayOp]) extends Op
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
The `ArrayOp` cases:
|
|
548
|
+
|
|
549
|
+
| Case | Parameters | Effect |
|
|
550
|
+
|------|-----------|--------|
|
|
551
|
+
| `ArrayOp.Insert(index, values)` | position, elements | Inserts `values` before `index` |
|
|
552
|
+
| `ArrayOp.Append(values)` | elements | Appends `values` to the end |
|
|
553
|
+
| `ArrayOp.Delete(index, count)` | position, count | Removes `count` elements starting at `index` |
|
|
554
|
+
| `ArrayOp.Modify(index, op)` | position, op | Applies `op` to the element at `index` |
|
|
555
|
+
|
|
556
|
+
Multiple `ArrayOp`s in a single `ArrayEdit` can be combined — here we transform `[1, 2, 3]` into `[0, 1, 2, 4]` in one pass:
|
|
557
|
+
|
|
558
|
+
```scala
|
|
559
|
+
import zio.blocks.schema.json.{Json, JsonPatch}
|
|
560
|
+
import zio.blocks.schema.json.JsonPatch._
|
|
561
|
+
import zio.blocks.chunk.Chunk
|
|
562
|
+
|
|
563
|
+
val arrayPatch = JsonPatch.root(Op.ArrayEdit(Chunk(
|
|
564
|
+
ArrayOp.Insert(0, Chunk(Json.Number(0))),
|
|
565
|
+
ArrayOp.Delete(3, 1),
|
|
566
|
+
ArrayOp.Append(Chunk(Json.Number(4)))
|
|
567
|
+
)))
|
|
568
|
+
val originalArr = Json.Array(Json.Number(1), Json.Number(2), Json.Number(3))
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
```scala
|
|
572
|
+
arrayPatch.apply(originalArr)
|
|
573
|
+
// res35: Either[SchemaError, Json] = Right(
|
|
574
|
+
// Array(IndexedSeq(Number(0), Number(1), Number(2), Number(4)))
|
|
575
|
+
// )
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
:::note
|
|
579
|
+
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.
|
|
580
|
+
:::
|
|
581
|
+
|
|
582
|
+
### `Op.ObjectEdit`
|
|
583
|
+
|
|
584
|
+
Applies a sequence of `ObjectOp` operations to a `Json.Object`. Operations are applied in order:
|
|
585
|
+
|
|
586
|
+
```scala
|
|
587
|
+
final case class ObjectEdit(ops: Chunk[ObjectOp]) extends Op
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
The `ObjectOp` cases:
|
|
591
|
+
|
|
592
|
+
| Case | Parameters | Effect |
|
|
593
|
+
|------|-----------|--------|
|
|
594
|
+
| `ObjectOp.Add(key, value)` | field name, value | Adds a new field; fails in `Strict` mode if key exists |
|
|
595
|
+
| `ObjectOp.Remove(key)` | field name | Removes an existing field |
|
|
596
|
+
| `ObjectOp.Modify(key, patch)` | field name, sub-patch | Applies `patch` recursively to the field value |
|
|
597
|
+
|
|
598
|
+
A single `ObjectEdit` can add, remove, and modify fields in one operation:
|
|
599
|
+
|
|
600
|
+
```scala
|
|
601
|
+
import zio.blocks.schema.json.{Json, JsonPatch}
|
|
602
|
+
import zio.blocks.schema.json.JsonPatch._
|
|
603
|
+
import zio.blocks.chunk.Chunk
|
|
604
|
+
|
|
605
|
+
val originalObj = Json.Object(
|
|
606
|
+
"name" -> Json.String("Alice"),
|
|
607
|
+
"age" -> Json.Number(25),
|
|
608
|
+
"city" -> Json.String("NYC")
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
val objPatch = JsonPatch.root(Op.ObjectEdit(Chunk(
|
|
612
|
+
ObjectOp.Add("email", Json.String("alice@example.com")),
|
|
613
|
+
ObjectOp.Remove("city"),
|
|
614
|
+
ObjectOp.Modify("age", JsonPatch.root(Op.PrimitiveDelta(PrimitiveOp.NumberDelta(BigDecimal(1)))))
|
|
615
|
+
)))
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
```scala
|
|
619
|
+
objPatch.apply(originalObj)
|
|
620
|
+
// res37: Either[SchemaError, Json] = Right(
|
|
621
|
+
// Object(
|
|
622
|
+
// IndexedSeq(
|
|
623
|
+
// ("name", String("Alice")),
|
|
624
|
+
// ("age", Number(26)),
|
|
625
|
+
// ("email", String("alice@example.com"))
|
|
626
|
+
// )
|
|
627
|
+
// )
|
|
628
|
+
// )
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
### `Op.Nested`
|
|
632
|
+
|
|
633
|
+
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`:
|
|
634
|
+
|
|
635
|
+
```scala
|
|
636
|
+
final case class Nested(patch: JsonPatch) extends Op
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
You rarely need to construct `Nested` manually; it is primarily an internal optimization used by the diff algorithm.
|
|
640
|
+
|
|
641
|
+
## Diffing Algorithm
|
|
642
|
+
|
|
643
|
+
`JsonPatch.diff` (and its alias `Json#diff`) delegate to `JsonDiffer.diff`, which selects the most compact representation for each type of change:
|
|
644
|
+
|
|
645
|
+
| Value type | Change | Strategy |
|
|
646
|
+
|------------|--------|----------|
|
|
647
|
+
| Any | No change | No operation emitted |
|
|
648
|
+
| `Json.Number` | Value changed | `NumberDelta` — stores the numeric difference |
|
|
649
|
+
| `Json.String` | Value changed | `StringEdit` via LCS if smaller; otherwise `Set` |
|
|
650
|
+
| `Json.Array` | Elements changed | `ArrayEdit` with LCS-aligned `Insert`/`Delete`/`Append`/`Modify` |
|
|
651
|
+
| `Json.Object` | Fields changed | `ObjectEdit` with recursive per-field diff |
|
|
652
|
+
| Any | Type changed | `Set` — full replacement |
|
|
653
|
+
|
|
654
|
+
:::tip
|
|
655
|
+
`JsonPatch.diff` followed by `JsonPatch#apply` is always a lossless roundtrip: for any `source` and `target`, `JsonPatch.diff(source, target).apply(source) == Right(target)`.
|
|
656
|
+
:::
|
|
657
|
+
|
|
658
|
+
## JsonPatch vs RFC 6902 JSON Patch
|
|
659
|
+
|
|
660
|
+
ZIO Blocks' `JsonPatch` is **not** an implementation of [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902). The two share the same motivation but differ in design:
|
|
661
|
+
|
|
662
|
+
| | ZIO Blocks `JsonPatch` | RFC 6902 JSON Patch |
|
|
663
|
+
|---|---|---|
|
|
664
|
+
| Operations | Typed ADT (`Op.Set`, `Op.ArrayEdit`, …) | String-tagged JSON objects (`"op": "replace"`) |
|
|
665
|
+
| Paths | `DynamicOptic` (typed, composable) | JSON Pointer strings (`"/a/b/0"`) |
|
|
666
|
+
| Number changes | `NumberDelta` (stores diff) | `replace` (stores full new value) |
|
|
667
|
+
| String changes | LCS-based `StringEdit` | `replace` only |
|
|
668
|
+
| Array changes | LCS-aligned insert/delete | `add`, `remove`, `replace` at absolute indices |
|
|
669
|
+
| Serialization | Via ZIO Blocks `Schema` in any format | Always JSON |
|
|
670
|
+
| Composition | `++` operator | Array concatenation |
|
|
671
|
+
|
|
672
|
+
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.
|
|
673
|
+
|
|
674
|
+
## Advanced Usage
|
|
675
|
+
|
|
676
|
+
`JsonPatch`'s composability and first-class serializability unlock patterns beyond simple point-in-time updates.
|
|
677
|
+
|
|
678
|
+
### Building a Change Log
|
|
679
|
+
|
|
680
|
+
Because `JsonPatch` is a pure value with a `Schema`, we can serialize every change and replay or audit it later:
|
|
681
|
+
|
|
682
|
+
```scala
|
|
683
|
+
import zio.blocks.schema.json.{Json, JsonPatch}
|
|
684
|
+
|
|
685
|
+
// Every mutation is a patch — store it instead of overwriting
|
|
686
|
+
val v0 = Json.Object("count" -> Json.Number(0))
|
|
687
|
+
val v1 = Json.Object("count" -> Json.Number(1))
|
|
688
|
+
val v2 = Json.Object("count" -> Json.Number(2))
|
|
689
|
+
|
|
690
|
+
val log: List[JsonPatch] = List(
|
|
691
|
+
JsonPatch.diff(v0, v1),
|
|
692
|
+
JsonPatch.diff(v1, v2)
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
// Replay: reconstruct any historical state
|
|
696
|
+
val replay = log.foldLeft(v0: Json)((state, patch) => patch.apply(state).getOrElse(state))
|
|
697
|
+
assert(replay == v2)
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
### Composing Targeted Sub-Patches
|
|
701
|
+
|
|
702
|
+
We can build a single patch that updates multiple nested fields by combining focused per-field patches with `++`:
|
|
703
|
+
|
|
704
|
+
```scala
|
|
705
|
+
import zio.blocks.schema.json.{Json, JsonPatch}
|
|
706
|
+
import zio.blocks.schema.json.JsonPatch._
|
|
707
|
+
import zio.blocks.schema.DynamicOptic
|
|
708
|
+
|
|
709
|
+
def setField(field: String, value: Json): JsonPatch =
|
|
710
|
+
JsonPatch(DynamicOptic.root.field(field), Op.Set(value))
|
|
711
|
+
|
|
712
|
+
val fieldPatch =
|
|
713
|
+
setField("status", Json.String("active")) ++
|
|
714
|
+
setField("updatedAt", Json.String("2025-01-01"))
|
|
715
|
+
|
|
716
|
+
val doc = Json.Object("status" -> Json.String("draft"), "id" -> Json.Number(42))
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
Applying the composed patch updates both fields in one step:
|
|
720
|
+
|
|
721
|
+
```scala
|
|
722
|
+
fieldPatch.apply(doc)
|
|
723
|
+
// res40: Either[SchemaError, Json] = Left(
|
|
724
|
+
// SchemaError(
|
|
725
|
+
// List(
|
|
726
|
+
// MissingField(source = DynamicOptic(ArraySeq()), fieldName = "updatedAt")
|
|
727
|
+
// )
|
|
728
|
+
// )
|
|
729
|
+
// )
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
## Integration
|
|
733
|
+
|
|
734
|
+
`JsonPatch` integrates with `Json`, `DynamicPatch`, and the ZIO Blocks serialization system. Each integration point is covered below.
|
|
735
|
+
|
|
736
|
+
### With `Json`
|
|
737
|
+
|
|
738
|
+
`Json` exposes two extension methods as entry points into `JsonPatch`:
|
|
739
|
+
|
|
740
|
+
```scala
|
|
741
|
+
import zio.blocks.schema.json.{Json, JsonPatch}
|
|
742
|
+
import zio.blocks.schema.patch.PatchMode
|
|
743
|
+
|
|
744
|
+
val source = Json.Object("x" -> Json.Number(1))
|
|
745
|
+
val target = Json.Object("x" -> Json.Number(2))
|
|
746
|
+
|
|
747
|
+
val patch: JsonPatch = source.diff(target) // compute patch
|
|
748
|
+
val result = source.patch(patch) // apply (Strict)
|
|
749
|
+
val lenient = source.patch(patch, PatchMode.Lenient)
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
See [Json](./json.md) for the complete `Json` API.
|
|
753
|
+
|
|
754
|
+
### With `DynamicPatch`
|
|
755
|
+
|
|
756
|
+
`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:
|
|
757
|
+
|
|
758
|
+
```scala
|
|
759
|
+
import zio.blocks.schema.json.{Json, JsonPatch}
|
|
760
|
+
import zio.blocks.schema.patch.DynamicPatch
|
|
761
|
+
import zio.blocks.schema.SchemaError
|
|
762
|
+
|
|
763
|
+
val jsonPatch: JsonPatch = JsonPatch.diff(Json.Number(1), Json.Number(3))
|
|
764
|
+
|
|
765
|
+
// JsonPatch → DynamicPatch (always succeeds)
|
|
766
|
+
val dynPatch: DynamicPatch = jsonPatch.toDynamicPatch
|
|
767
|
+
|
|
768
|
+
// DynamicPatch → JsonPatch (may fail for temporal ops or non-string keys)
|
|
769
|
+
val back: Either[SchemaError, JsonPatch] = JsonPatch.fromDynamicPatch(dynPatch)
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
See [Patching](./patch.md) for the typed `Patch[S]` API.
|
|
773
|
+
|
|
774
|
+
### Serialization
|
|
775
|
+
|
|
776
|
+
`JsonPatch` ships with `Schema` instances for all nested operation types, enabling round-trip serialization via any ZIO Blocks codec:
|
|
777
|
+
|
|
778
|
+
```scala
|
|
779
|
+
import zio.blocks.schema.json.JsonPatch
|
|
780
|
+
import zio.blocks.schema.Schema
|
|
781
|
+
|
|
782
|
+
val schema: Schema[JsonPatch] = implicitly[Schema[JsonPatch]]
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
See [Codec & Format](./codec.md) for how to derive and use codecs.
|
|
786
|
+
|
|
787
|
+
## Examples
|
|
788
|
+
|
|
789
|
+
Runnable examples are in `schema-examples/src/main/scala/jsonpatch/`:
|
|
790
|
+
|
|
791
|
+
| File | Topic |
|
|
792
|
+
|------|-------|
|
|
793
|
+
| `JsonPatchDiffAndApplyExample.scala` | `JsonPatch.diff`, `Json#diff`, `Json#patch`, roundtrip guarantee |
|
|
794
|
+
| `JsonPatchManualBuildExample.scala` | `JsonPatch.root`, path-based patches, `JsonPatch.empty` |
|
|
795
|
+
| `JsonPatchOperationsExample.scala` | All `Op` types — `Set`, `NumberDelta`, `StringEdit`, `ArrayEdit`, `ObjectEdit` |
|
|
796
|
+
| `JsonPatchCompositionExample.scala` | `++`, `PatchMode`, `toDynamicPatch`, `fromDynamicPatch` |
|
|
797
|
+
| `CompleteJsonPatchExample.scala` | Collaborative document editor with a full patch log, replay, and sync |
|
|
798
|
+
|
|
799
|
+
Run any example with:
|
|
800
|
+
|
|
801
|
+
```bash
|
|
802
|
+
sbt "schema-examples/runMain jsonpatch.CompleteJsonPatchExample"
|
|
803
|
+
```
|