@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,823 @@
1
+ ---
2
+ id: dynamic-value
3
+ title: "DynamicValue"
4
+ ---
5
+
6
+ `DynamicValue` is a schema-less, dynamically-typed representation of any structured value in ZIO Blocks. It provides a universal data model that can represent any value without requiring compile-time type information, serving as an intermediate representation for serialization, schema evolution, data transformation, and cross-format conversion.
7
+
8
+ ## Overview
9
+
10
+ The `DynamicValue` type represents all structured values with six cases:
11
+
12
+ ```
13
+ DynamicValue
14
+ ├── DynamicValue.Primitive (scalar values: strings, numbers, booleans, temporal types, etc.)
15
+ ├── DynamicValue.Record (named fields, analogous to case classes or JSON objects)
16
+ ├── DynamicValue.Variant (tagged unions, analogous to sealed traits)
17
+ ├── DynamicValue.Sequence (ordered collections: lists, arrays, vectors)
18
+ ├── DynamicValue.Map (key-value pairs where keys are also DynamicValues)
19
+ └── DynamicValue.Null (absence of a value)
20
+ ```
21
+
22
+ Key design decisions:
23
+
24
+ - **Type-agnostic** — Works without compile-time type information
25
+ - **Preserves structure** — Maintains full fidelity of the original data
26
+ - **Supports rich primitives** — All Java time types, BigDecimal, UUID, Currency, etc.
27
+ - **Path-based navigation** — Uses `DynamicOptic` for traversal and modification
28
+ - **EJSON toString** — Human-readable output format with type annotations
29
+
30
+ ## DynamicValue Variants
31
+
32
+ ### Primitive
33
+
34
+ Wraps scalar values in a `PrimitiveValue`:
35
+
36
+ ```scala mdoc:compile-only
37
+ import zio.blocks.schema.DynamicValue
38
+
39
+ // Using convenience constructors
40
+ val str = DynamicValue.string("hello")
41
+ val num = DynamicValue.int(42)
42
+ val flag = DynamicValue.boolean(true)
43
+ val pi = DynamicValue.double(3.14159)
44
+
45
+ // Using the Primitive case directly
46
+ import zio.blocks.schema.PrimitiveValue
47
+ val instant = DynamicValue.Primitive(
48
+ PrimitiveValue.Instant(java.time.Instant.now())
49
+ )
50
+ ```
51
+
52
+ ### Record
53
+
54
+ A collection of named fields, analogous to case classes or JSON objects:
55
+
56
+ ```scala mdoc:compile-only
57
+ import zio.blocks.schema.DynamicValue
58
+ import zio.blocks.chunk.Chunk
59
+
60
+ // Using varargs constructor
61
+ val person = DynamicValue.Record(
62
+ "name" -> DynamicValue.string("Alice"),
63
+ "age" -> DynamicValue.int(30),
64
+ "active" -> DynamicValue.boolean(true)
65
+ )
66
+
67
+ // Using Chunk constructor
68
+ val point = DynamicValue.Record(Chunk(
69
+ ("x", DynamicValue.int(10)),
70
+ ("y", DynamicValue.int(20))
71
+ ))
72
+
73
+ // Empty record
74
+ val empty = DynamicValue.Record.empty
75
+ ```
76
+
77
+ Field order is preserved and significant for equality. Use `sortFields` to normalize for order-independent comparison.
78
+
79
+ ### Variant
80
+
81
+ A tagged union value, analogous to sealed traits:
82
+
83
+ ```scala mdoc:compile-only
84
+ import zio.blocks.schema.DynamicValue
85
+
86
+ // A Some variant containing a value
87
+ val some = DynamicValue.Variant(
88
+ "Some",
89
+ DynamicValue.string("hello")
90
+ )
91
+
92
+ // A None variant with an empty record
93
+ val none = DynamicValue.Variant("None", DynamicValue.Record.empty)
94
+
95
+ // Access case information
96
+ some.caseName // Some("Some")
97
+ some.caseValue // Some(DynamicValue.Primitive(...))
98
+ ```
99
+
100
+ ### Sequence
101
+
102
+ An ordered collection of values:
103
+
104
+ ```scala mdoc:compile-only
105
+ import zio.blocks.schema.DynamicValue
106
+ import zio.blocks.chunk.Chunk
107
+
108
+ // Using varargs constructor
109
+ val numbers = DynamicValue.Sequence(
110
+ DynamicValue.int(1),
111
+ DynamicValue.int(2),
112
+ DynamicValue.int(3)
113
+ )
114
+
115
+ // Using Chunk constructor
116
+ val items = DynamicValue.Sequence(Chunk(
117
+ DynamicValue.string("a"),
118
+ DynamicValue.string("b")
119
+ ))
120
+
121
+ // Empty sequence
122
+ val empty = DynamicValue.Sequence.empty
123
+ ```
124
+
125
+ ### Map
126
+
127
+ Key-value pairs where both keys and values are `DynamicValue`:
128
+
129
+ ```scala mdoc:compile-only
130
+ import zio.blocks.schema.DynamicValue
131
+ import zio.blocks.chunk.Chunk
132
+
133
+ // String keys (common case)
134
+ val config = DynamicValue.Map(
135
+ DynamicValue.string("host") -> DynamicValue.string("localhost"),
136
+ DynamicValue.string("port") -> DynamicValue.int(8080)
137
+ )
138
+
139
+ // Non-string keys (unlike Record)
140
+ val mapping = DynamicValue.Map(
141
+ DynamicValue.int(1) -> DynamicValue.string("one"),
142
+ DynamicValue.int(2) -> DynamicValue.string("two")
143
+ )
144
+
145
+ // Empty map
146
+ val empty = DynamicValue.Map.empty
147
+ ```
148
+
149
+ Unlike `Record` which uses String keys, `Map` supports arbitrary `DynamicValue` keys.
150
+
151
+ ### Null
152
+
153
+ Represents the absence of a value:
154
+
155
+ ```scala mdoc:compile-only
156
+ import zio.blocks.schema.DynamicValue
157
+
158
+ val absent = DynamicValue.Null
159
+ ```
160
+
161
+ ## PrimitiveValue Types
162
+
163
+ `PrimitiveValue` is a sealed trait representing all scalar values that can be wrapped in `DynamicValue.Primitive`. Each case preserves full type information:
164
+
165
+ | Type | Description | Example |
166
+ |------|-------------|---------|
167
+ | `Unit` | Unit value | `PrimitiveValue.Unit` |
168
+ | `Boolean` | Boolean | `PrimitiveValue.Boolean(true)` |
169
+ | `Byte` | 8-bit integer | `PrimitiveValue.Byte(127)` |
170
+ | `Short` | 16-bit integer | `PrimitiveValue.Short(32767)` |
171
+ | `Int` | 32-bit integer | `PrimitiveValue.Int(42)` |
172
+ | `Long` | 64-bit integer | `PrimitiveValue.Long(9999999999L)` |
173
+ | `Float` | 32-bit float | `PrimitiveValue.Float(3.14f)` |
174
+ | `Double` | 64-bit float | `PrimitiveValue.Double(3.14159)` |
175
+ | `Char` | Unicode character | `PrimitiveValue.Char('A')` |
176
+ | `String` | Text | `PrimitiveValue.String("hello")` |
177
+ | `BigInt` | Arbitrary precision integer | `PrimitiveValue.BigInt(BigInt("999..."))` |
178
+ | `BigDecimal` | Arbitrary precision decimal | `PrimitiveValue.BigDecimal(BigDecimal("3.14159"))` |
179
+ | `Instant` | Timestamp | `PrimitiveValue.Instant(Instant.now())` |
180
+ | `LocalDate` | Date without time | `PrimitiveValue.LocalDate(LocalDate.now())` |
181
+ | `LocalDateTime` | Date and time | `PrimitiveValue.LocalDateTime(LocalDateTime.now())` |
182
+ | `LocalTime` | Time without date | `PrimitiveValue.LocalTime(LocalTime.now())` |
183
+ | `Duration` | Time duration | `PrimitiveValue.Duration(Duration.ofHours(1))` |
184
+ | `Period` | Date-based period | `PrimitiveValue.Period(Period.ofDays(30))` |
185
+ | `DayOfWeek` | Day of week | `PrimitiveValue.DayOfWeek(DayOfWeek.MONDAY)` |
186
+ | `Month` | Month | `PrimitiveValue.Month(Month.JANUARY)` |
187
+ | `Year` | Year | `PrimitiveValue.Year(Year.of(2024))` |
188
+ | `YearMonth` | Year and month | `PrimitiveValue.YearMonth(YearMonth.of(2024, 1))` |
189
+ | `MonthDay` | Month and day | `PrimitiveValue.MonthDay(MonthDay.of(1, 15))` |
190
+ | `ZoneId` | Time zone | `PrimitiveValue.ZoneId(ZoneId.of("UTC"))` |
191
+ | `ZoneOffset` | Time zone offset | `PrimitiveValue.ZoneOffset(ZoneOffset.UTC)` |
192
+ | `ZonedDateTime` | Date/time with zone | `PrimitiveValue.ZonedDateTime(ZonedDateTime.now())` |
193
+ | `OffsetDateTime` | Date/time with offset | `PrimitiveValue.OffsetDateTime(OffsetDateTime.now())` |
194
+ | `OffsetTime` | Time with offset | `PrimitiveValue.OffsetTime(OffsetTime.now())` |
195
+ | `UUID` | Universally unique ID | `PrimitiveValue.UUID(UUID.randomUUID())` |
196
+ | `Currency` | Currency | `PrimitiveValue.Currency(Currency.getInstance("USD"))` |
197
+
198
+ ## Creating DynamicValues from Typed Values
199
+
200
+ Use `Schema.toDynamicValue` to convert typed Scala values to `DynamicValue`:
201
+
202
+ ```scala mdoc:compile-only
203
+ import zio.blocks.schema.{Schema, DynamicValue}
204
+
205
+ case class Person(name: String, age: Int)
206
+ object Person {
207
+ implicit val schema: Schema[Person] = Schema.derived
208
+ }
209
+
210
+ val person = Person("Alice", 30)
211
+ val dynamic: DynamicValue = Schema[Person].toDynamicValue(person)
212
+ // Record with "name" and "age" fields
213
+
214
+ // Works with any type that has a Schema
215
+ val listDynamic = Schema[List[Int]].toDynamicValue(List(1, 2, 3))
216
+ // Sequence of Primitive(Int) values
217
+ ```
218
+
219
+ ## Converting DynamicValues Back to Typed Values
220
+
221
+ Use `Schema.fromDynamicValue` to convert `DynamicValue` back to typed Scala values:
222
+
223
+ ```scala mdoc:compile-only
224
+ import zio.blocks.schema.{Schema, DynamicValue, SchemaError}
225
+
226
+ case class Person(name: String, age: Int)
227
+ object Person {
228
+ implicit val schema: Schema[Person] = Schema.derived
229
+ }
230
+
231
+ val dynamic = DynamicValue.Record(
232
+ "name" -> DynamicValue.string("Bob"),
233
+ "age" -> DynamicValue.int(25)
234
+ )
235
+
236
+ val result: Either[SchemaError, Person] = Schema[Person].fromDynamicValue(dynamic)
237
+ // Right(Person("Bob", 25))
238
+
239
+ // Type mismatch produces an error
240
+ val badDynamic = DynamicValue.string("not a person")
241
+ val error = Schema[Person].fromDynamicValue(badDynamic)
242
+ // Left(SchemaError(...))
243
+ ```
244
+
245
+ ## Type Information
246
+
247
+ ### DynamicValueType
248
+
249
+ Each `DynamicValue` has a corresponding `DynamicValueType` for runtime type checking:
250
+
251
+ ```scala mdoc:compile-only
252
+ import zio.blocks.schema.{DynamicValue, DynamicValueType}
253
+
254
+ val dv = DynamicValue.Record("x" -> DynamicValue.int(1))
255
+
256
+ // Check type
257
+ dv.is(DynamicValueType.Record) // true
258
+ dv.is(DynamicValueType.Sequence) // false
259
+
260
+ // Narrow to specific type
261
+ val record: Option[DynamicValue.Record] = dv.as(DynamicValueType.Record)
262
+ // Some(Record(...))
263
+
264
+ // Extract underlying value
265
+ import zio.blocks.chunk.Chunk
266
+ val fields: Option[Chunk[(String, DynamicValue)]] =
267
+ dv.unwrap(DynamicValueType.Record)
268
+ ```
269
+
270
+ ### Extracting Primitive Values
271
+
272
+ ```scala mdoc:compile-only
273
+ import zio.blocks.schema.{DynamicValue, PrimitiveType, Validation}
274
+
275
+ val dv = DynamicValue.int(42)
276
+
277
+ // Extract with specific primitive type
278
+ val intValue: Option[Int] = dv.asPrimitive(PrimitiveType.Int(Validation.None))
279
+ // Some(42)
280
+
281
+ val stringValue: Option[String] = dv.asPrimitive(PrimitiveType.String(Validation.None))
282
+ // None (type mismatch)
283
+ ```
284
+
285
+ ## Navigation
286
+
287
+ ### Simple Navigation
288
+
289
+ Navigate using `get` methods that return `DynamicValueSelection`:
290
+
291
+ ```scala mdoc:compile-only
292
+ import zio.blocks.schema.DynamicValue
293
+
294
+ val data = DynamicValue.Record(
295
+ "users" -> DynamicValue.Sequence(
296
+ DynamicValue.Record("name" -> DynamicValue.string("Alice")),
297
+ DynamicValue.Record("name" -> DynamicValue.string("Bob"))
298
+ )
299
+ )
300
+
301
+ // Navigate to a field
302
+ val users = data.get("users") // DynamicValueSelection
303
+
304
+ // Navigate to an array element
305
+ val firstUser = data.get("users").apply(0)
306
+
307
+ // Chain navigation
308
+ val firstName = data.get("users").apply(0).get("name")
309
+
310
+ // Extract the value
311
+ val name = firstName.one // Either[SchemaError, DynamicValue]
312
+ ```
313
+
314
+ ### Path-Based Navigation with DynamicOptic
315
+
316
+ Use `DynamicOptic` for complex path expressions:
317
+
318
+ ```scala mdoc:compile-only
319
+ import zio.blocks.schema.{DynamicValue, DynamicOptic}
320
+
321
+ val data = DynamicValue.Record(
322
+ "company" -> DynamicValue.Record(
323
+ "employees" -> DynamicValue.Sequence(
324
+ DynamicValue.Record("name" -> DynamicValue.string("Alice"))
325
+ )
326
+ )
327
+ )
328
+
329
+ // Build a path
330
+ val path = DynamicOptic.root.field("company").field("employees").at(0).field("name")
331
+
332
+ // Navigate using the path
333
+ val result = data.get(path).one // Right(DynamicValue.Primitive(String("Alice")))
334
+ ```
335
+
336
+ ### DynamicValueSelection
337
+
338
+ `DynamicValueSelection` wraps navigation results and provides fluent chaining:
339
+
340
+ ```scala mdoc:compile-only
341
+ import zio.blocks.schema.{DynamicValue, DynamicValueSelection}
342
+
343
+ val selection: DynamicValueSelection = ???
344
+
345
+ // Terminal operations
346
+ selection.one // Either[SchemaError, DynamicValue] - exactly one value
347
+ selection.any // Either[SchemaError, DynamicValue] - first of many
348
+ selection.all // Either[SchemaError, DynamicValue] - wrap multiple in Sequence
349
+ selection.toChunk // Chunk[DynamicValue] - empty on error
350
+
351
+ // Type filtering
352
+ selection.primitives // Only Primitive values
353
+ selection.records // Only Record values
354
+ selection.sequences // Only Sequence values
355
+ selection.maps // Only Map values
356
+
357
+ // Combinators
358
+ selection.map(dv => ???) // Transform values
359
+ selection.filter(dv => ???) // Filter values
360
+ selection.flatMap(dv => ???) // Chain selections
361
+ ```
362
+
363
+ ## Path-Based Modification
364
+
365
+ ### Modify
366
+
367
+ Update values at a path:
368
+
369
+ ```scala mdoc:compile-only
370
+ import zio.blocks.schema.{DynamicValue, DynamicOptic}
371
+
372
+ val data = DynamicValue.Record(
373
+ "user" -> DynamicValue.Record(
374
+ "name" -> DynamicValue.string("Alice")
375
+ )
376
+ )
377
+
378
+ val path = DynamicOptic.root.field("user").field("name")
379
+
380
+ // Modify value at path
381
+ val updated = data.modify(path)(dv => DynamicValue.string("Bob"))
382
+ // Record("user" -> Record("name" -> "Bob"))
383
+ ```
384
+
385
+ ### Set
386
+
387
+ Replace a value at a path:
388
+
389
+ ```scala mdoc:compile-only
390
+ import zio.blocks.schema.{DynamicValue, DynamicOptic}
391
+
392
+ val data = DynamicValue.Record("x" -> DynamicValue.int(1))
393
+ val path = DynamicOptic.root.field("x")
394
+
395
+ val updated = data.set(path, DynamicValue.int(99))
396
+ // Record("x" -> 99)
397
+ ```
398
+
399
+ ### Delete
400
+
401
+ Remove a value at a path:
402
+
403
+ ```scala mdoc:compile-only
404
+ import zio.blocks.schema.{DynamicValue, DynamicOptic}
405
+
406
+ val data = DynamicValue.Record(
407
+ "a" -> DynamicValue.int(1),
408
+ "b" -> DynamicValue.int(2)
409
+ )
410
+
411
+ val updated = data.delete(DynamicOptic.root.field("a"))
412
+ // Record("b" -> 2)
413
+ ```
414
+
415
+ ### Insert
416
+
417
+ Add a value at a path (fails if path exists):
418
+
419
+ ```scala mdoc:compile-only
420
+ import zio.blocks.schema.{DynamicValue, DynamicOptic}
421
+
422
+ val data = DynamicValue.Record("a" -> DynamicValue.int(1))
423
+
424
+ val updated = data.insert(
425
+ DynamicOptic.root.field("b"),
426
+ DynamicValue.int(2)
427
+ )
428
+ // Record("a" -> 1, "b" -> 2)
429
+ ```
430
+
431
+ ### Fallible Operations
432
+
433
+ Use `*OrFail` variants for operations that should fail explicitly:
434
+
435
+ ```scala mdoc:compile-only
436
+ import zio.blocks.schema.{DynamicValue, DynamicOptic, SchemaError}
437
+
438
+ val data = DynamicValue.Record("x" -> DynamicValue.int(1))
439
+ val badPath = DynamicOptic.root.field("nonexistent")
440
+
441
+ val result: Either[SchemaError, DynamicValue] =
442
+ data.setOrFail(badPath, DynamicValue.int(99))
443
+ // Left(SchemaError("Path not found"))
444
+ ```
445
+
446
+ ## EJSON-like toString Format
447
+
448
+ `DynamicValue.toString` produces an EJSON (Extended JSON) format that:
449
+
450
+ - Uses unquoted field names for Records (like Scala syntax)
451
+ - Uses quoted string keys for Maps
452
+ - Adds `@ {tag: "..."}` annotations for Variants
453
+ - Adds `@ {type: "..."}` annotations for typed primitives (Instant, Duration, etc.)
454
+
455
+ ```scala mdoc:compile-only
456
+ import zio.blocks.schema.{DynamicValue, PrimitiveValue}
457
+
458
+ val person = DynamicValue.Record(
459
+ "name" -> DynamicValue.string("Alice"),
460
+ "age" -> DynamicValue.int(30)
461
+ )
462
+
463
+ println(person.toString)
464
+ // {
465
+ // name: "Alice",
466
+ // age: 30
467
+ // }
468
+
469
+ val variant = DynamicValue.Variant(
470
+ "Some",
471
+ DynamicValue.string("hello")
472
+ )
473
+
474
+ println(variant.toString)
475
+ // "hello" @ {tag: "Some"}
476
+
477
+ val timestamp = DynamicValue.Primitive(
478
+ PrimitiveValue.Instant(java.time.Instant.ofEpochMilli(1700000000000L))
479
+ )
480
+
481
+ println(timestamp.toString)
482
+ // 1700000000000 @ {type: "instant"}
483
+ ```
484
+
485
+ Use `toEjson(indent)` to control indentation level.
486
+
487
+ ## Merging Strategies
488
+
489
+ Merge two `DynamicValue` structures using configurable strategies:
490
+
491
+ ```scala mdoc:compile-only
492
+ import zio.blocks.schema.{DynamicValue, DynamicValueMergeStrategy}
493
+
494
+ val left = DynamicValue.Record(
495
+ "a" -> DynamicValue.int(1),
496
+ "b" -> DynamicValue.int(2)
497
+ )
498
+
499
+ val right = DynamicValue.Record(
500
+ "b" -> DynamicValue.int(99),
501
+ "c" -> DynamicValue.int(3)
502
+ )
503
+
504
+ // Deep merge (default): recursively merge containers
505
+ val merged = left.merge(right, DynamicValueMergeStrategy.Auto)
506
+ // Record("a" -> 1, "b" -> 99, "c" -> 3)
507
+ ```
508
+
509
+ ### Available Strategies
510
+
511
+ | Strategy | Behavior |
512
+ |----------|----------|
513
+ | `Auto` | Deep merge: Records by field, Sequences by index, Maps by key. Right wins at leaves. |
514
+ | `Replace` | Complete replacement: right value replaces left entirely |
515
+ | `KeepLeft` | Always keep left value |
516
+ | `Shallow` | Merge only at root level, nested containers replaced |
517
+ | `Concat` | Concatenate Sequences instead of merging by index |
518
+ | `Custom(f, r)` | Custom function with custom recursion control |
519
+
520
+ ```scala mdoc:compile-only
521
+ import zio.blocks.schema.{DynamicValue, DynamicValueMergeStrategy}
522
+
523
+ val list1 = DynamicValue.Sequence(DynamicValue.int(1), DynamicValue.int(2))
524
+ val list2 = DynamicValue.Sequence(DynamicValue.int(3))
525
+
526
+ // Concat sequences instead of index-based merge
527
+ val concatted = list1.merge(list2, DynamicValueMergeStrategy.Concat)
528
+ // Sequence(1, 2, 3)
529
+ ```
530
+
531
+ ## Normalization
532
+
533
+ Transform `DynamicValue` structures for comparison or serialization:
534
+
535
+ ```scala mdoc:compile-only
536
+ import zio.blocks.schema.DynamicValue
537
+
538
+ val data = DynamicValue.Record(
539
+ "z" -> DynamicValue.int(1),
540
+ "a" -> DynamicValue.Null,
541
+ "m" -> DynamicValue.int(2)
542
+ )
543
+
544
+ // Sort fields alphabetically
545
+ data.sortFields
546
+ // Record("a" -> null, "m" -> 2, "z" -> 1)
547
+
548
+ // Remove null values
549
+ data.dropNulls
550
+ // Record("z" -> 1, "m" -> 2)
551
+
552
+ // Remove empty containers
553
+ data.dropEmpty
554
+
555
+ // Remove Unit primitives
556
+ data.dropUnits
557
+
558
+ // Apply all normalizations
559
+ data.normalize
560
+ // Sorted, no nulls, no units, no empty containers
561
+ ```
562
+
563
+ ## Transformation
564
+
565
+ ### Transform Up/Down
566
+
567
+ Apply functions to all values in a structure:
568
+
569
+ ```scala mdoc:compile-only
570
+ import zio.blocks.schema.{DynamicValue, DynamicOptic, PrimitiveValue}
571
+
572
+ val data = DynamicValue.Record(
573
+ "values" -> DynamicValue.Sequence(
574
+ DynamicValue.int(1),
575
+ DynamicValue.int(2)
576
+ )
577
+ )
578
+
579
+ // Bottom-up: children transformed before parents
580
+ val doubled = data.transformUp { (path, dv) =>
581
+ dv match {
582
+ case DynamicValue.Primitive(pv: PrimitiveValue.Int) =>
583
+ DynamicValue.int(pv.value * 2)
584
+ case other => other
585
+ }
586
+ }
587
+
588
+ // Top-down: parents transformed before children
589
+ val topDown = data.transformDown { (path, dv) => ??? }
590
+ ```
591
+
592
+ ### Transform Field Names
593
+
594
+ Rename all record fields:
595
+
596
+ ```scala mdoc:compile-only
597
+ import zio.blocks.schema.{DynamicValue, DynamicOptic}
598
+
599
+ val data = DynamicValue.Record(
600
+ "first_name" -> DynamicValue.string("Alice"),
601
+ "last_name" -> DynamicValue.string("Smith")
602
+ )
603
+
604
+ // Convert snake_case to camelCase
605
+ val camelCase = data.transformFields { (path, name) =>
606
+ name.split("_").zipWithIndex.map {
607
+ case (word, 0) => word
608
+ case (word, _) => word.capitalize
609
+ }.mkString
610
+ }
611
+ // Record("firstName" -> "Alice", "lastName" -> "Smith")
612
+ ```
613
+
614
+ ## Folding
615
+
616
+ Aggregate values from a `DynamicValue` tree:
617
+
618
+ ```scala mdoc:compile-only
619
+ import zio.blocks.schema.{DynamicValue, DynamicOptic, PrimitiveValue}
620
+
621
+ val data = DynamicValue.Record(
622
+ "a" -> DynamicValue.int(1),
623
+ "b" -> DynamicValue.int(2),
624
+ "c" -> DynamicValue.int(3)
625
+ )
626
+
627
+ // Sum all integers
628
+ val sum = data.foldUp(0) { (path, dv, acc) =>
629
+ dv match {
630
+ case DynamicValue.Primitive(pv: PrimitiveValue.Int) => acc + pv.value
631
+ case _ => acc
632
+ }
633
+ }
634
+ // 6
635
+ ```
636
+
637
+ ## Converting to/from JSON
638
+
639
+ ### To JSON
640
+
641
+ ```scala mdoc:compile-only
642
+ import zio.blocks.schema.DynamicValue
643
+ import zio.blocks.schema.json.Json
644
+
645
+ val dynamic = DynamicValue.Record(
646
+ "name" -> DynamicValue.string("Alice"),
647
+ "age" -> DynamicValue.int(30)
648
+ )
649
+
650
+ val json: Json = dynamic.toJson
651
+ // Json.Object with "name" and "age" fields
652
+ ```
653
+
654
+ ### From JSON
655
+
656
+ ```scala mdoc:compile-only
657
+ import zio.blocks.schema.DynamicValue
658
+ import zio.blocks.schema.json.Json
659
+
660
+ val json = Json.parseUnsafe("""{"name": "Alice", "age": 30}""")
661
+
662
+ val dynamic: DynamicValue = json.toDynamicValue
663
+ // DynamicValue.Record with "name" and "age" fields
664
+ ```
665
+
666
+ ## Querying
667
+
668
+ Search recursively for values matching a predicate:
669
+
670
+ ```scala mdoc:compile-only
671
+ import zio.blocks.schema.{DynamicValue, DynamicValueType, PrimitiveValue}
672
+
673
+ val data = DynamicValue.Record(
674
+ "users" -> DynamicValue.Sequence(
675
+ DynamicValue.Record("name" -> DynamicValue.string("Alice"), "active" -> DynamicValue.boolean(true)),
676
+ DynamicValue.Record("name" -> DynamicValue.string("Bob"), "active" -> DynamicValue.boolean(false))
677
+ )
678
+ )
679
+
680
+ // Find all string values
681
+ val strings = data.select.query(_.is(DynamicValueType.Primitive))
682
+ .filter(_.primitiveValue.exists(_.isInstanceOf[PrimitiveValue.String]))
683
+
684
+ // Query with path predicate
685
+ val atDepth2 = data.select.queryPath(path => path.nodes.length == 2)
686
+ ```
687
+
688
+ ## Use Cases
689
+
690
+ ### Schema-less Operations
691
+
692
+ Work with data when the schema isn't known at compile time:
693
+
694
+ ```scala mdoc:compile-only
695
+ import zio.blocks.schema.{DynamicValue, PrimitiveValue}
696
+
697
+ def processAnyData(data: DynamicValue): DynamicValue = {
698
+ // Add a timestamp to any record
699
+ data match {
700
+ case r: DynamicValue.Record =>
701
+ DynamicValue.Record(
702
+ r.fields :+ ("processedAt" -> DynamicValue.Primitive(
703
+ PrimitiveValue.Instant(java.time.Instant.now())
704
+ ))
705
+ )
706
+ case other => other
707
+ }
708
+ }
709
+ ```
710
+
711
+ ### Schema Migrations
712
+
713
+ Transform data between schema versions:
714
+
715
+ ```scala mdoc:compile-only
716
+ import zio.blocks.schema.{DynamicValue, DynamicOptic}
717
+
718
+ def migrateV1toV2(data: DynamicValue): DynamicValue = {
719
+ data.transformFields { (path, name) =>
720
+ // Rename deprecated field
721
+ if (name == "userName") "name"
722
+ else name
723
+ }.transformUp { (path, dv) =>
724
+ // Add default for new required field
725
+ dv match {
726
+ case r: DynamicValue.Record if path.nodes.isEmpty =>
727
+ DynamicValue.Record(r.fields :+ ("version" -> DynamicValue.int(2)))
728
+ case other => other
729
+ }
730
+ }
731
+ }
732
+ ```
733
+
734
+ ### Dynamic Queries
735
+
736
+ Build queries at runtime:
737
+
738
+ ```scala mdoc:compile-only
739
+ import zio.blocks.schema.{DynamicValue, DynamicOptic}
740
+
741
+ def buildPath(fields: List[String]): DynamicOptic =
742
+ fields.foldLeft(DynamicOptic.root)(_.field(_))
743
+
744
+ def getValue(data: DynamicValue, path: List[String]): Option[DynamicValue] =
745
+ data.get(buildPath(path)).one.toOption
746
+
747
+ // Usage
748
+ val data = DynamicValue.Record(
749
+ "user" -> DynamicValue.Record(
750
+ "profile" -> DynamicValue.Record(
751
+ "email" -> DynamicValue.string("alice@example.com")
752
+ )
753
+ )
754
+ )
755
+
756
+ val email = getValue(data, List("user", "profile", "email"))
757
+ // Some(DynamicValue.Primitive(String("alice@example.com")))
758
+ ```
759
+
760
+ ### Cross-Format Conversion
761
+
762
+ Use `DynamicValue` as an intermediate format:
763
+
764
+ ```scala mdoc:compile-only
765
+ import zio.blocks.schema.{Schema, DynamicValue}
766
+ import zio.blocks.schema.json.Json
767
+
768
+ case class Person(name: String, age: Int)
769
+ object Person {
770
+ implicit val schema: Schema[Person] = Schema.derived
771
+ }
772
+
773
+ // JSON -> DynamicValue -> Typed
774
+ val json = Json.parseUnsafe("""{"name": "Alice", "age": 30}""")
775
+ val dynamic = json.toDynamicValue
776
+ val person = Schema[Person].fromDynamicValue(dynamic)
777
+
778
+ // Typed -> DynamicValue -> JSON
779
+ val dynamic2 = Schema[Person].toDynamicValue(Person("Bob", 25))
780
+ val json2 = dynamic2.toJson
781
+ ```
782
+
783
+ ## Comparison and Ordering
784
+
785
+ `DynamicValue` has a total ordering for sorting and comparison:
786
+
787
+ ```scala mdoc:compile-only
788
+ import zio.blocks.schema.DynamicValue
789
+
790
+ val a = DynamicValue.int(1)
791
+ val b = DynamicValue.int(2)
792
+
793
+ a.compare(b) // negative
794
+ a < b // true
795
+ a >= b // false
796
+
797
+ // Type ordering: Primitive < Record < Variant < Sequence < Map < Null
798
+ val primitive = DynamicValue.int(1)
799
+ val record = DynamicValue.Record.empty
800
+ primitive < record // true
801
+ ```
802
+
803
+ ## Diff and Patch
804
+
805
+ Compute differences between `DynamicValue` instances:
806
+
807
+ ```scala mdoc:compile-only
808
+ import zio.blocks.schema.DynamicValue
809
+ import zio.blocks.schema.patch.DynamicPatch
810
+
811
+ val old = DynamicValue.Record(
812
+ "name" -> DynamicValue.string("Alice"),
813
+ "age" -> DynamicValue.int(30)
814
+ )
815
+
816
+ val new_ = DynamicValue.Record(
817
+ "name" -> DynamicValue.string("Alice"),
818
+ "age" -> DynamicValue.int(31)
819
+ )
820
+
821
+ val patch: DynamicPatch = old.diff(new_)
822
+ // Patch that updates "age" from 30 to 31
823
+ ```