@zio.dev/zio-blocks 0.0.1

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.
@@ -0,0 +1,979 @@
1
+ ---
2
+ id: json
3
+ title: "Json"
4
+ ---
5
+
6
+ `Json` is an algebraic data type (ADT) for representing JSON values in ZIO Blocks. It provides a type-safe, schema-free way to work with JSON data, enabling navigation, transformation, merging, and querying without losing fidelity.
7
+
8
+ ## Overview
9
+
10
+ The `Json` type represents all valid JSON values with six cases:
11
+
12
+ ```
13
+ Json
14
+ ├── Json.Object (key-value pairs, order-preserving)
15
+ ├── Json.Array (ordered sequence of values)
16
+ ├── Json.String (text)
17
+ ├── Json.Number (arbitrary precision via BigDecimal)
18
+ ├── Json.Boolean (true/false)
19
+ └── Json.Null (null)
20
+ ```
21
+
22
+ Key design decisions:
23
+
24
+ - **Objects use `Vector[(String, Json)]`** to preserve insertion order while providing order-independent equality
25
+ - **Numbers use `BigDecimal`** to preserve precision for financial and scientific data
26
+ - **All navigation returns `JsonSelection`** for fluent, composable chaining
27
+
28
+ ## Creating JSON Values
29
+
30
+ ### Using Constructors
31
+
32
+ ```scala mdoc:compile-only
33
+ import zio.blocks.schema.json.Json
34
+
35
+ // Object with named fields
36
+ val person = Json.Object(
37
+ "name" -> Json.String("Alice"),
38
+ "age" -> Json.Number(30),
39
+ "active" -> Json.Boolean(true)
40
+ )
41
+
42
+ // Array of values
43
+ val numbers = Json.Array(Json.Number(1), Json.Number(2), Json.Number(3))
44
+
45
+ // Primitive values
46
+ val name = Json.String("Bob")
47
+ val count = Json.Number(42)
48
+ val flag = Json.Boolean(false)
49
+ val nothing = Json.Null
50
+ ```
51
+
52
+ ### Parsing JSON Strings
53
+
54
+ ```scala mdoc:compile-only
55
+ import zio.blocks.schema.json.Json
56
+ import zio.blocks.schema.SchemaError
57
+
58
+ // Safe parsing (returns Either)
59
+ val parsed: Either[SchemaError, Json] = Json.parse("""{"name": "Alice", "age": 30}""")
60
+
61
+ // Unsafe parsing (throws on error)
62
+ val json = Json.parseUnsafe("""{"items": [1, 2, 3]}""")
63
+ ```
64
+
65
+ ### String Interpolators
66
+
67
+ ZIO Blocks provides compile-time validated string interpolators for JSON:
68
+
69
+ ```scala mdoc:compile-only
70
+ import zio.blocks.schema._
71
+ import zio.blocks.schema.json._
72
+
73
+ // JSON literal with compile-time validation
74
+ val person = json"""{"name": "Alice", "age": 30}"""
75
+
76
+ // With Scala value interpolation
77
+ val name = "Bob"
78
+ val age = 25
79
+ val person2 = json"""{"name": $name, "age": $age}"""
80
+
81
+ // Path interpolator for navigation
82
+ val path = p".users[0].name"
83
+ ```
84
+
85
+ The `json"..."` interpolator validates JSON syntax at compile time, catching errors before runtime.
86
+
87
+ ## Type Testing and Access
88
+
89
+ ### Unified Type Operations
90
+
91
+ The `Json` type provides unified methods for type testing and narrowing with path-dependent return types.
92
+ `JsonType` also implements `Json => Boolean`, so it can be used directly as a predicate for filtering.
93
+
94
+ ```scala mdoc:compile-only
95
+ import zio.blocks.schema.json.{Json, JsonType}
96
+
97
+ val json: Json = Json.parseUnsafe("""{"count": 42}""")
98
+
99
+ // Type testing with is()
100
+ json.is(JsonType.Object) // true
101
+ json.is(JsonType.Array) // false
102
+
103
+ // Type narrowing with as() - returns Option[jsonType.Type]
104
+ val obj: Option[Json.Object] = json.as(JsonType.Object) // Some(Json.Object(...))
105
+ val arr: Option[Json.Array] = json.as(JsonType.Array) // None
106
+
107
+ // Value extraction with unwrap() - returns Option[jsonType.Unwrap]
108
+ val str: Json = Json.String("hello")
109
+ val strValue: Option[String] = str.unwrap(JsonType.String) // Some("hello")
110
+
111
+ val num: Json = Json.Number(42)
112
+ val numValue: Option[BigDecimal] = num.unwrap(JsonType.Number) // Some(42)
113
+
114
+ // JsonType as predicate - use directly in selection query
115
+ val strings = json.select.query(JsonType.String) // all string values in the JSON tree
116
+ ```
117
+
118
+ ### Direct Value Access
119
+
120
+ ```scala mdoc:compile-only
121
+ import zio.blocks.schema.json.Json
122
+
123
+ val obj = Json.Object("a" -> Json.Number(1))
124
+ obj.fields // Chunk(("a", Json.Number(1)))
125
+
126
+ val arr = Json.Array(Json.Number(1), Json.Number(2))
127
+ arr.elements // Chunk(Json.Number(1), Json.Number(2))
128
+ ```
129
+
130
+ ## Navigation
131
+
132
+ ### Simple Navigation
133
+
134
+ Navigate into objects by key and arrays by index:
135
+
136
+ ```scala mdoc:compile-only
137
+ import zio.blocks.schema.json.Json
138
+ import zio.blocks.schema.SchemaError
139
+
140
+ val json = Json.parseUnsafe("""{
141
+ "users": [
142
+ {"name": "Alice", "age": 30},
143
+ {"name": "Bob", "age": 25}
144
+ ]
145
+ }""")
146
+
147
+ // Navigate to a field
148
+ val users = json.get("users") // JsonSelection
149
+
150
+ // Navigate to an array element
151
+ val firstUser = json.get("users")(0) // JsonSelection
152
+
153
+ // Chain navigation
154
+ val firstName = json.get("users")(0).get("name") // JsonSelection
155
+
156
+ // Extract the value
157
+ val name: Either[SchemaError, String] = firstName.as[String] // Right("Alice")
158
+ ```
159
+
160
+ ### Path-Based Navigation with DynamicOptic
161
+
162
+ Use `DynamicOptic` paths for complex navigation:
163
+
164
+ ```scala mdoc:compile-only
165
+ import zio.blocks.schema._
166
+ import zio.blocks.schema.json._
167
+
168
+ val json = Json.parseUnsafe("""{
169
+ "company": {
170
+ "employees": [
171
+ {"name": "Alice", "department": "Engineering"},
172
+ {"name": "Bob", "department": "Sales"}
173
+ ]
174
+ }
175
+ }""")
176
+
177
+ // Using path interpolator
178
+ val path = p".company.employees[0].name"
179
+ val name = json.get(path).as[String] // Right("Alice")
180
+
181
+ // Equivalent to chained navigation
182
+ val sameName = json.get("company").get("employees")(0).get("name").as[String]
183
+ ```
184
+
185
+ ## JsonSelection
186
+
187
+ `JsonSelection` is a fluent wrapper for navigation results, enabling composable chaining:
188
+
189
+ ```scala mdoc:compile-only
190
+ import zio.blocks.schema.json.{Json, JsonSelection}
191
+
192
+ val json = Json.parseUnsafe("""{"users": [{"name": "Alice"}]}""")
193
+
194
+ // Fluent chaining
195
+ val result: JsonSelection = json
196
+ .get("users")
197
+ .arrays
198
+ .apply(0)
199
+ .get("name")
200
+ .strings
201
+
202
+ // Extract values
203
+ result.as[String] // Right("Alice")
204
+ result.one // Right(Json.String("Alice"))
205
+ result.isSuccess // true
206
+ result.isFailure // false
207
+ ```
208
+
209
+ ### Terminal Operations
210
+
211
+ ```scala mdoc:compile-only
212
+ import zio.blocks.schema.json.{Json, JsonSelection}
213
+ import zio.blocks.schema.SchemaError
214
+
215
+ val selection: JsonSelection = ???
216
+
217
+ // Get single value (exactly one required)
218
+ val oneValue: Either[SchemaError, Json] = selection.one
219
+ // Get any single value (first of many)
220
+ val anyValue: Either[SchemaError, Json] = selection.any
221
+ // Get all values condensed (wraps multiple in array)
222
+ val allValues: Either[SchemaError, Json] = selection.all
223
+
224
+ // Get underlying result
225
+ val underlying: Either[SchemaError, Vector[Json]] = selection.either
226
+ val asVector: Vector[Json] = selection.toVector // empty on error
227
+
228
+ // Decode to specific types
229
+ val asString: Either[SchemaError, String] = selection.as[String]
230
+ val asBigDecimal: Either[SchemaError, BigDecimal] = selection.as[BigDecimal]
231
+ val asBoolean: Either[SchemaError, Boolean] = selection.as[Boolean]
232
+ val asInt: Either[SchemaError, Int] = selection.as[Int]
233
+ val asLong: Either[SchemaError, Long] = selection.as[Long]
234
+ val asDouble: Either[SchemaError, Double] = selection.as[Double]
235
+ ```
236
+
237
+ ## Modification
238
+
239
+ ### Setting Values
240
+
241
+ ```scala mdoc:compile-only
242
+ import zio.blocks.schema._
243
+ import zio.blocks.schema.json._
244
+
245
+ val json = Json.parseUnsafe("""{"user": {"name": "Alice", "age": 30}}""")
246
+
247
+ // Set a value at a path
248
+ val updated = json.set(p".user.name", Json.String("Bob"))
249
+ // {"user": {"name": "Bob", "age": 30}}
250
+
251
+ // Set with failure handling
252
+ val result = json.setOrFail(p".user.email", Json.String("alice@example.com"))
253
+ // Left(SchemaError) - path doesn't exist
254
+ ```
255
+
256
+ ### Modifying Values
257
+
258
+ ```scala mdoc:compile-only
259
+ import zio.blocks.schema._
260
+ import zio.blocks.schema.json._
261
+
262
+ val json = Json.parseUnsafe("""{"count": 10}""")
263
+
264
+ // Modify with a function
265
+ val incremented = json.modify(p".count") {
266
+ case Json.Number(n) => Json.Number(n + 1)
267
+ case other => other
268
+ }
269
+ // {"count": 11}
270
+
271
+ // Modify with failure on missing path
272
+ val result = json.modifyOrFail(p".count") {
273
+ case Json.Number(n) => Json.Number(n * 2)
274
+ }
275
+ // Right({"count": 20})
276
+ ```
277
+
278
+ ### Deleting Values
279
+
280
+ ```scala mdoc:compile-only
281
+ import zio.blocks.schema._
282
+ import zio.blocks.schema.json._
283
+
284
+ val json = Json.parseUnsafe("""{"a": 1, "b": 2, "c": 3}""")
285
+
286
+ // Delete a field
287
+ val withoutB = json.delete(p".b")
288
+ // {"a": 1, "c": 3}
289
+
290
+ // Delete with failure handling
291
+ val result = json.deleteOrFail(p".missing")
292
+ // Left(SchemaError) - path doesn't exist
293
+ ```
294
+
295
+ ### Inserting Values
296
+
297
+ ```scala mdoc:compile-only
298
+ import zio.blocks.schema._
299
+ import zio.blocks.schema.json._
300
+
301
+ val json = Json.parseUnsafe("""{"existing": 1}""")
302
+
303
+ // Insert a new field
304
+ val withNew = json.insert(p".newField", Json.String("value"))
305
+ // {"existing": 1, "newField": "value"}
306
+ ```
307
+
308
+ ## Transformation
309
+
310
+ ### Transform Up (Bottom-Up)
311
+
312
+ Transform children before parents:
313
+
314
+ ```scala mdoc:compile-only
315
+ import zio.blocks.schema.json.Json
316
+ import zio.blocks.schema.DynamicOptic
317
+
318
+ val json = Json.parseUnsafe("""{"values": [1, 2, 3]}""")
319
+
320
+ // Double all numbers
321
+ val doubled = json.transformUp { (path, value) =>
322
+ value match {
323
+ case Json.Number(n) => Json.Number(n * 2)
324
+ case other => other
325
+ }
326
+ }
327
+ // {"values": [2, 4, 6]}
328
+ ```
329
+
330
+ ### Transform Down (Top-Down)
331
+
332
+ Transform parents before children:
333
+
334
+ ```scala mdoc:compile-only
335
+ import zio.blocks.schema.json.Json
336
+ import zio.blocks.schema.DynamicOptic
337
+
338
+ val json = Json.parseUnsafe("""{"items": [{"x": 1}, {"x": 2}]}""")
339
+
340
+ // Add a field to all objects
341
+ val withId = json.transformDown { (path, value) =>
342
+ value match {
343
+ case Json.Object(fields) if !fields.exists(_._1 == "id") =>
344
+ new Json.Object(("id" -> Json.String(path.toString)) +: fields)
345
+ case other => other
346
+ }
347
+ }
348
+ ```
349
+
350
+ ### Transform Keys
351
+
352
+ Rename object keys throughout the structure:
353
+
354
+ ```scala mdoc:compile-only
355
+ import zio.blocks.schema.json.Json
356
+
357
+ val json = Json.parseUnsafe("""{"user_name": "Alice", "user_age": 30}""")
358
+
359
+ // Convert snake_case to camelCase
360
+ val camelCase = json.transformKeys { (path, key) =>
361
+ key.split("_").zipWithIndex.map {
362
+ case (word, 0) => word
363
+ case (word, _) => word.capitalize
364
+ }.mkString
365
+ }
366
+ // {"userName": "Alice", "userAge": 30}
367
+ ```
368
+
369
+ ## Filtering
370
+
371
+ ### Filter Values
372
+
373
+ Keep only values matching a predicate using `retain`, or remove values using `prune`:
374
+
375
+ ```scala mdoc:compile-only
376
+ import zio.blocks.schema.json.{Json, JsonType}
377
+
378
+ val json = Json.parseUnsafe("""{"a": 1, "b": null, "c": 2, "d": null}""")
379
+
380
+ // Remove nulls using prune (removes values matching predicate)
381
+ val noNulls = json.prune(_.is(JsonType.Null))
382
+ // {"a": 1, "c": 2}
383
+
384
+ // Keep only numbers using retain (keeps values matching predicate)
385
+ val onlyNumbers = json.retain(_.is(JsonType.Number))
386
+ // {"a": 1, "c": 2}
387
+ ```
388
+
389
+ ### Project Paths
390
+
391
+ Extract only specific paths:
392
+
393
+ ```scala mdoc:compile-only
394
+ import zio.blocks.schema._
395
+ import zio.blocks.schema.json._
396
+
397
+ val json = Json.parseUnsafe("""{
398
+ "user": {"name": "Alice", "email": "alice@example.com", "password": "secret"},
399
+ "metadata": {"created": "2024-01-01"}
400
+ }""")
401
+
402
+ // Keep only specific fields
403
+ val projected = json.project(p".user.name", p".user.email")
404
+ // {"user": {"name": "Alice", "email": "alice@example.com"}}
405
+ ```
406
+
407
+ ### Partition
408
+
409
+ Split based on a predicate:
410
+
411
+ ```scala mdoc:compile-only
412
+ import zio.blocks.schema.json.{Json, JsonType}
413
+
414
+ val json = Json.parseUnsafe("""{"a": 1, "b": "text", "c": 2}""")
415
+
416
+ // Separate numbers from non-numbers
417
+ val (numbers, nonNumbers) = json.partition(_.is(JsonType.Number))
418
+ // numbers: {"a": 1, "c": 2}
419
+ // nonNumbers: {"b": "text"}
420
+ ```
421
+
422
+ ## Folding
423
+
424
+ ### Fold Up (Bottom-Up)
425
+
426
+ Accumulate values from children to parents:
427
+
428
+ ```scala mdoc:compile-only
429
+ import zio.blocks.schema.json.Json
430
+
431
+ val json = Json.parseUnsafe("""{"values": [1, 2, 3, 4, 5]}""")
432
+
433
+ // Sum all numbers
434
+ val sum = json.foldUp(BigDecimal(0)) { (path, value, acc) =>
435
+ value match {
436
+ case n: Json.Number => acc + n.toBigDecimal
437
+ case _ => acc
438
+ }
439
+ }
440
+ // sum = 15
441
+ ```
442
+
443
+ ### Fold Down (Top-Down)
444
+
445
+ Accumulate values from parents to children:
446
+
447
+ ```scala mdoc:compile-only
448
+ import zio.blocks.schema.json.Json
449
+ import zio.blocks.schema.DynamicOptic
450
+
451
+ val json = Json.parseUnsafe("""{"a": {"b": {"c": 1}}}""")
452
+
453
+ // Collect all paths
454
+ val paths = json.foldDown(Vector.empty[DynamicOptic]) { (path, value, acc) =>
455
+ acc :+ path
456
+ }
457
+ ```
458
+
459
+ ## Merging
460
+
461
+ Combine two JSON values using different strategies:
462
+
463
+ ```scala mdoc:compile-only
464
+ import zio.blocks.schema.json.{Json, MergeStrategy}
465
+
466
+ val base = Json.parseUnsafe("""{"a": 1, "b": {"x": 10}}""")
467
+ val overlay = Json.parseUnsafe("""{"b": {"y": 20}, "c": 3}""")
468
+
469
+ // Auto strategy (default) - deep merge objects, concat arrays
470
+ val merged = base.merge(overlay)
471
+ // {"a": 1, "b": {"x": 10, "y": 20}, "c": 3}
472
+
473
+ // Shallow merge (only top-level)
474
+ val shallow = base.merge(overlay, MergeStrategy.Shallow)
475
+
476
+ // Replace (right wins)
477
+ val replaced = base.merge(overlay, MergeStrategy.Replace)
478
+ // {"b": {"y": 20}, "c": 3}
479
+
480
+ // Concat arrays
481
+ val concat = base.merge(overlay, MergeStrategy.Concat)
482
+
483
+ // Custom strategy
484
+ val custom = base.merge(overlay, MergeStrategy.Custom { (path, left, right) =>
485
+ // Your merge logic here
486
+ right
487
+ })
488
+ ```
489
+
490
+ ### Merge Strategies
491
+
492
+ | Strategy | Objects | Arrays | Primitives |
493
+ |----------|---------|--------|------------|
494
+ | `Auto` | Deep merge | Concatenate | Replace |
495
+ | `Deep` | Recursive merge | Concatenate | Replace |
496
+ | `Shallow` | Top-level only | Concatenate | Replace |
497
+ | `Replace` | Right wins | Right wins | Right wins |
498
+ | `Concat` | Merge keys | Concatenate | Replace |
499
+ | `Custom(f)` | User-defined | User-defined | User-defined |
500
+
501
+ ## Normalization
502
+
503
+ Clean up JSON values:
504
+
505
+ ```scala mdoc:compile-only
506
+ import zio.blocks.schema.json.Json
507
+
508
+ val json = Json.parseUnsafe("""{
509
+ "z": 1,
510
+ "a": null,
511
+ "m": {"empty": {}},
512
+ "b": 2
513
+ }""")
514
+
515
+ // Sort object keys alphabetically
516
+ val sorted = json.sortKeys
517
+ // {"a": null, "b": 2, "m": {"empty": {}}, "z": 1}
518
+
519
+ // Remove null values
520
+ val noNulls = json.dropNulls
521
+ // {"z": 1, "m": {"empty": {}}, "b": 2}
522
+
523
+ // Remove empty objects and arrays
524
+ val noEmpty = json.dropEmpty
525
+ // {"z": 1, "a": null, "b": 2}
526
+
527
+ // Apply all normalizations
528
+ val normalized = json.normalize
529
+ // {"b": 2, "z": 1}
530
+ ```
531
+
532
+ ## Encoding and Decoding
533
+
534
+ ### Type Classes
535
+
536
+ ZIO Blocks provides `JsonEncoder` and `JsonDecoder` type classes for converting between Scala types and `Json`:
537
+
538
+ ```scala mdoc:compile-only
539
+ import zio.blocks.schema.json.{Json, JsonEncoder, JsonDecoder}
540
+
541
+ // Encode Scala values to Json
542
+ val intJson = JsonEncoder[Int].encode(42) // Json.Number(42)
543
+ val strJson = JsonEncoder[String].encode("hello") // Json.String("hello")
544
+
545
+ // Decode Json to Scala values
546
+ val intResult = JsonDecoder[Int].decode(Json.Number(42)) // Right(42)
547
+ val strResult = JsonDecoder[String].decode(Json.String("hello")) // Right("hello")
548
+ ```
549
+
550
+ ### Built-in Encoders/Decoders
551
+
552
+ ```scala mdoc:compile-only
553
+ import zio.blocks.schema.json.{JsonEncoder, JsonDecoder}
554
+
555
+ // Primitives
556
+ JsonEncoder[String]
557
+ JsonEncoder[Int]
558
+ JsonEncoder[Long]
559
+ JsonEncoder[Double]
560
+ JsonEncoder[Boolean]
561
+ JsonEncoder[BigDecimal]
562
+
563
+ // Collections
564
+ JsonEncoder[List[Int]]
565
+ JsonEncoder[Vector[String]]
566
+ JsonEncoder[Map[String, Int]]
567
+ JsonEncoder[Option[String]]
568
+
569
+ // Java time types
570
+ JsonEncoder[java.time.Instant]
571
+ JsonEncoder[java.time.LocalDate]
572
+ JsonEncoder[java.time.ZonedDateTime]
573
+ JsonEncoder[java.util.UUID]
574
+ ```
575
+
576
+ ### Schema-Based Derivation
577
+
578
+ For complex types, use Schema-based derivation:
579
+
580
+ ```scala mdoc:compile-only
581
+ import zio.blocks.schema.Schema
582
+ import zio.blocks.schema.json.{Json, JsonEncoder, JsonDecoder}
583
+
584
+ case class Person(name: String, age: Int)
585
+
586
+ object Person {
587
+ implicit val schema: Schema[Person] = Schema.derived
588
+
589
+ // Derived from schema (lower priority)
590
+ implicit val encoder: JsonEncoder[Person] = JsonEncoder.fromSchema
591
+ implicit val decoder: JsonDecoder[Person] = JsonDecoder.fromSchema
592
+ }
593
+
594
+ val person = Person("Alice", 30)
595
+ val json = JsonEncoder[Person].encode(person)
596
+ val decoded = JsonDecoder[Person].decode(json)
597
+ ```
598
+
599
+ ### Extension Syntax
600
+
601
+ When a `Schema` is in scope, you can use convenient extension methods directly on values:
602
+
603
+ ```scala mdoc:compile-only
604
+ import zio.blocks.schema._
605
+
606
+ case class Person(name: String, age: Int)
607
+ object Person {
608
+ implicit val schema: Schema[Person] = Schema.derived
609
+ }
610
+
611
+ val person = Person("Alice", 30)
612
+
613
+ // Convert to Json AST
614
+ val json = person.toJson // Json.Object(...)
615
+
616
+ // Convert directly to JSON string
617
+ val jsonString = person.toJsonString // {"name":"Alice","age":30}
618
+
619
+ // Convert to UTF-8 bytes
620
+ val jsonBytes = person.toJsonBytes // Array[Byte]
621
+
622
+ // Parse JSON string back to a typed value
623
+ val parsed = """{"name":"Bob","age":25}""".fromJson[Person] // Right(Person("Bob", 25))
624
+
625
+ // Parse from bytes
626
+ val fromBytes = jsonBytes.fromJson[Person] // Right(Person("Alice", 30))
627
+ ```
628
+
629
+ These extension methods provide a more ergonomic API compared to explicitly creating encoders/decoders.
630
+
631
+ ### Using the `as` Method
632
+
633
+ ```scala mdoc:compile-only
634
+ import zio.blocks.schema.json.Json
635
+ import zio.blocks.schema.json.JsonDecoder
636
+ import zio.blocks.schema.{Schema, SchemaError}
637
+
638
+ case class Person(name: String, age: Int)
639
+ object Person {
640
+ implicit val schema: Schema[Person] = Schema.derived
641
+ implicit val decoder: JsonDecoder[Person] = JsonDecoder.fromSchema
642
+ }
643
+
644
+ val json = Json.parseUnsafe("""{"name": "Alice", "age": 30}""")
645
+
646
+ // Decode to a specific type
647
+ val person: Either[SchemaError, Person] = json.as[Person]
648
+
649
+ // Unsafe version (throws on error)
650
+ val personUnsafe: Person = json.asUnsafe[Person]
651
+ ```
652
+
653
+ ## Printing JSON
654
+
655
+ ### Basic Printing
656
+
657
+ ```scala mdoc:compile-only
658
+ import zio.blocks.schema.json.Json
659
+
660
+ val json = Json.Object("name" -> Json.String("Alice"), "age" -> Json.Number(30))
661
+
662
+ // Compact output
663
+ val compact: String = json.print
664
+ // {"name":"Alice","age":30}
665
+ ```
666
+
667
+ ### With Writer Config
668
+
669
+ ```scala mdoc:compile-only
670
+ import zio.blocks.schema.json.Json
671
+ import zio.blocks.schema.json.WriterConfig
672
+
673
+ val json = Json.Object("name" -> Json.String("Alice"))
674
+
675
+ // Pretty-printed output (2-space indentation)
676
+ val pretty = json.print(WriterConfig.withIndentionStep2)
677
+ // {
678
+ // "name": "Alice"
679
+ // }
680
+
681
+ // Custom indentation
682
+ val indented4 = json.print(WriterConfig.withIndentionStep(4))
683
+ ```
684
+
685
+ ### WriterConfig Options
686
+
687
+ `WriterConfig` controls JSON output formatting:
688
+
689
+ | Option | Default | Description |
690
+ |--------|---------|-------------|
691
+ | `indentionStep` | `0` | Spaces per indentation level (0 = compact) |
692
+ | `escapeUnicode` | `false` | Escape non-ASCII characters as `\uXXXX` |
693
+ | `preferredBufSize` | `32768` | Internal buffer size in bytes |
694
+
695
+ ```scala mdoc:compile-only
696
+ import zio.blocks.schema.json.WriterConfig
697
+
698
+ // Compact output (default)
699
+ val compact = WriterConfig
700
+
701
+ // Pretty-printed with 2-space indentation
702
+ val pretty = WriterConfig.withIndentionStep(2)
703
+
704
+ // Escape Unicode for ASCII-only output
705
+ val ascii = WriterConfig.withEscapeUnicode(true)
706
+
707
+ // Combine options
708
+ val custom = WriterConfig
709
+ .withIndentionStep(2)
710
+ .withEscapeUnicode(true)
711
+ .withPreferredBufSize(65536)
712
+ ```
713
+
714
+ ### ReaderConfig Options
715
+
716
+ `ReaderConfig` controls JSON parsing behavior:
717
+
718
+ | Option | Default | Description |
719
+ |--------|---------|-------------|
720
+ | `preferredBufSize` | `32768` | Preferred byte buffer size |
721
+ | `preferredCharBufSize` | `4096` | Preferred char buffer size for strings |
722
+ | `maxBufSize` | `33554432` | Maximum byte buffer size (32MB) |
723
+ | `maxCharBufSize` | `4194304` | Maximum char buffer size (4MB) |
724
+ | `checkForEndOfInput` | `true` | Error on trailing non-whitespace |
725
+
726
+ ```scala mdoc:compile-only
727
+ import zio.blocks.schema.json.ReaderConfig
728
+
729
+ // Default configuration
730
+ val default = ReaderConfig
731
+
732
+ // Allow trailing content (useful for streaming)
733
+ val lenient = ReaderConfig.withCheckForEndOfInput(false)
734
+
735
+ // Increase buffer sizes for large documents
736
+ val largeDoc = ReaderConfig
737
+ .withPreferredBufSize(65536)
738
+ .withPreferredCharBufSize(8192)
739
+ ```
740
+
741
+ ### To Bytes
742
+
743
+ ```scala mdoc:compile-only
744
+ import zio.blocks.schema.json.Json
745
+
746
+ val json = Json.Object("x" -> Json.Number(1))
747
+
748
+ // As byte array
749
+ val bytes: Array[Byte] = json.printBytes
750
+ ```
751
+
752
+ ## Query Operations
753
+
754
+ ### Query with Predicate
755
+
756
+ Find all values matching a condition:
757
+
758
+ ```scala mdoc:compile-only
759
+ import zio.blocks.schema.json.Json
760
+
761
+ val json = Json.parseUnsafe("""{
762
+ "users": [
763
+ {"name": "Alice", "active": true},
764
+ {"name": "Bob", "active": false},
765
+ {"name": "Charlie", "active": true}
766
+ ]
767
+ }""")
768
+
769
+ // Find all active users using queryBoth on a selection
770
+ val activeUsers = json.select.queryBoth { (path, value) =>
771
+ value.get("active").as[Boolean].getOrElse(false)
772
+ }
773
+ ```
774
+
775
+ ### Convert to Key-Value Pairs
776
+
777
+ Flatten to path-value pairs:
778
+
779
+ ```scala mdoc:compile-only
780
+ import zio.blocks.schema.json.Json
781
+ import zio.blocks.schema.DynamicOptic
782
+ import zio.blocks.chunk.Chunk
783
+
784
+ val json = Json.parseUnsafe("""{"a": {"b": 1, "c": 2}}""")
785
+
786
+ val pairs: Chunk[(DynamicOptic, Json)] = json.toKV
787
+ // Chunk(
788
+ // ($.a.b, Json.Number(1)),
789
+ // ($.a.c, Json.Number(2))
790
+ // )
791
+ ```
792
+
793
+ ## Comparison and Equality
794
+
795
+ ### Object Equality
796
+
797
+ Objects are compared **order-independently** (keys are compared as sorted sets):
798
+
799
+ ```scala mdoc:compile-only
800
+ import zio.blocks.schema.json.Json
801
+
802
+ val obj1 = Json.parseUnsafe("""{"a": 1, "b": 2}""")
803
+ val obj2 = Json.parseUnsafe("""{"b": 2, "a": 1}""")
804
+
805
+ obj1 == obj2 // true (order-independent)
806
+ ```
807
+
808
+ ### Ordering
809
+
810
+ JSON values have a total ordering for sorting:
811
+
812
+ ```scala mdoc:compile-only
813
+ import zio.blocks.schema.json.Json
814
+
815
+ val values = List(
816
+ Json.String("z"),
817
+ Json.Number(1),
818
+ Json.Null,
819
+ Json.Boolean(true)
820
+ )
821
+
822
+ // Sort by type, then by value
823
+ val sorted = values.sortWith((a, b) => a.compare(b) < 0)
824
+ // [null, true, 1, "z"]
825
+ ```
826
+
827
+ Type ordering: Null < Boolean < Number < String < Array < Object
828
+
829
+ ## JSON Diffing
830
+
831
+ `JsonDiffer` computes the difference between two JSON values, producing a `JsonPatch` that transforms the source into the target:
832
+
833
+ ```scala mdoc:compile-only
834
+ import zio.blocks.schema.json.{Json, JsonPatch}
835
+
836
+ val source = Json.parseUnsafe("""{"name": "Alice", "age": 30}""")
837
+ val target = Json.parseUnsafe("""{"name": "Alice", "age": 31, "active": true}""")
838
+
839
+ // Compute the diff
840
+ val patch: JsonPatch = JsonPatch.diff(source, target)
841
+
842
+ // The patch describes the minimal changes:
843
+ // - NumberDelta for age: 30 -> 31
844
+ // - Add field "active": true
845
+ ```
846
+
847
+ The differ uses optimal operations:
848
+ - **NumberDelta** for numeric changes (stores the delta, not the new value)
849
+ - **StringEdit** for string changes when edits are more compact than replacement
850
+ - **ArrayEdit** with LCS-based Insert/Delete operations for arrays
851
+ - **ObjectEdit** with Add/Remove/Modify operations for objects
852
+
853
+ ## JSON Patching
854
+
855
+ `JsonPatch` represents a sequence of operations that transform a JSON value. Patches are composable and can be applied with different failure modes:
856
+
857
+ ### Computing and Applying Patches
858
+
859
+ ```scala mdoc:compile-only
860
+ import zio.blocks.schema.json.{Json, JsonPatch}
861
+ import zio.blocks.schema.patch.PatchMode
862
+ import zio.blocks.schema.SchemaError
863
+
864
+ val original = Json.parseUnsafe("""{"count": 10, "items": ["a", "b"]}""")
865
+ val modified = Json.parseUnsafe("""{"count": 15, "items": ["a", "b", "c"]}""")
866
+
867
+ // Compute the patch
868
+ val patch = JsonPatch.diff(original, modified)
869
+
870
+ // Apply with default (Strict) mode - fails on any precondition violation
871
+ val result1: Either[SchemaError, Json] = patch(original)
872
+
873
+ // Apply with Lenient mode - skips failing operations
874
+ val result2 = patch(original, PatchMode.Lenient)
875
+
876
+ // Apply with Clobber mode - forces changes on conflicts
877
+ val result3 = patch(original, PatchMode.Clobber)
878
+ ```
879
+
880
+ ### Patch Modes
881
+
882
+ | Mode | Behavior |
883
+ |------|----------|
884
+ | `Strict` | Fail immediately on any precondition violation |
885
+ | `Lenient` | Skip operations that fail preconditions |
886
+ | `Clobber` | Force changes, overwriting on conflicts |
887
+
888
+ ### Composing Patches
889
+
890
+ ```scala mdoc:compile-only
891
+ import zio.blocks.schema.json.{Json, JsonPatch}
892
+
893
+ val patch1 = JsonPatch.diff(
894
+ Json.parseUnsafe("""{"x": 1}"""),
895
+ Json.parseUnsafe("""{"x": 2}""")
896
+ )
897
+
898
+ val patch2 = JsonPatch.diff(
899
+ Json.parseUnsafe("""{"x": 2}"""),
900
+ Json.parseUnsafe("""{"x": 2, "y": 3}""")
901
+ )
902
+
903
+ // Compose patches - applies patch1, then patch2
904
+ val combined = patch1 ++ patch2
905
+
906
+ // Apply the combined patch
907
+ val result = combined(Json.parseUnsafe("""{"x": 1}"""))
908
+ // Right({"x": 2, "y": 3})
909
+ ```
910
+
911
+ ### Converting to DynamicPatch
912
+
913
+ `JsonPatch` can be converted to and from `DynamicPatch` for interoperability with the typed patching system:
914
+
915
+ ```scala mdoc:compile-only
916
+ import zio.blocks.schema.json.JsonPatch
917
+ import zio.blocks.schema.patch.DynamicPatch
918
+ import zio.blocks.schema.SchemaError
919
+
920
+ val jsonPatch: JsonPatch = ???
921
+
922
+ // Convert to DynamicPatch
923
+ val dynamicPatch: DynamicPatch = jsonPatch.toDynamicPatch
924
+
925
+ // Convert from DynamicPatch (may fail for unsupported operations)
926
+ val restored: Either[SchemaError, JsonPatch] = JsonPatch.fromDynamicPatch(dynamicPatch)
927
+ ```
928
+
929
+ ## Conversion to DynamicValue
930
+
931
+ Convert JSON to ZIO Blocks' semi-structured `DynamicValue`:
932
+
933
+ ```scala mdoc:compile-only
934
+ import zio.blocks.schema.json.Json
935
+ import zio.blocks.schema.DynamicValue
936
+
937
+ val json = Json.parseUnsafe("""{"name": "Alice"}""")
938
+
939
+ val dynamic: DynamicValue = json.toDynamicValue
940
+ ```
941
+
942
+ This enables interoperability with other ZIO Blocks formats (Avro, TOON, etc.).
943
+
944
+ ## Error Handling
945
+
946
+ ### SchemaError
947
+
948
+ Errors include path information for debugging:
949
+
950
+ ```scala mdoc:compile-only
951
+ import zio.blocks.schema.json.Json
952
+ import zio.blocks.schema.SchemaError
953
+
954
+ val json = Json.parseUnsafe("""{"users": [{"name": "Alice"}]}""")
955
+
956
+ val result = json.get("users")(5).get("name").as[String]
957
+ // Left(SchemaError: Index 5 out of bounds at path $.users[5])
958
+ ```
959
+
960
+ ### Error Properties
961
+
962
+ ```scala mdoc:compile-only
963
+ import zio.blocks.schema.SchemaError
964
+ import zio.blocks.schema.DynamicOptic
965
+
966
+ val error: SchemaError = ???
967
+
968
+ error.message // Error description
969
+ error.errors.head.source // DynamicOptic path to error location
970
+ ```
971
+
972
+ ## Cross-Platform Support
973
+
974
+ The `Json` type works across all platforms:
975
+
976
+ - **JVM** - Full functionality
977
+ - **Scala.js** - Browser and Node.js
978
+
979
+ String interpolators use compile-time validation that works on all platforms.