@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.
- package/index.md +426 -0
- package/package.json +6 -0
- package/path-interpolator.md +645 -0
- package/reference/binding.md +364 -0
- package/reference/chunk.md +576 -0
- package/reference/context.md +157 -0
- package/reference/docs.md +524 -0
- package/reference/dynamic-value.md +823 -0
- package/reference/formats.md +640 -0
- package/reference/json-schema.md +626 -0
- package/reference/json.md +979 -0
- package/reference/modifier.md +276 -0
- package/reference/optics.md +1613 -0
- package/reference/patch.md +631 -0
- package/reference/reflect-transform.md +387 -0
- package/reference/reflect.md +521 -0
- package/reference/registers.md +282 -0
- package/reference/schema-evolution.md +540 -0
- package/reference/schema.md +619 -0
- package/reference/syntax.md +409 -0
- package/reference/type-class-derivation-internals.md +632 -0
- package/reference/typeid.md +900 -0
- package/reference/validation.md +458 -0
- package/scope.md +627 -0
- package/sidebars.js +30 -0
|
@@ -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
|
+
```
|