@zio.dev/zio-blocks 0.0.21 → 0.0.22
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 +49 -29
- package/package.json +1 -1
- package/path-interpolator.md +24 -23
- package/reference/codec.md +384 -0
- package/reference/docs.md +1 -1
- package/reference/dynamic-optic.md +392 -0
- package/reference/formats.md +68 -12
- package/reference/json-schema.md +13 -10
- package/reference/lazy.md +361 -0
- package/reference/modifier.md +340 -0
- package/reference/syntax.md +11 -11
- package/reference/type-class-derivation.md +1959 -0
- package/scope.md +230 -232
- package/sidebars.js +6 -1
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: dynamic-optic
|
|
3
|
+
title: "DynamicOptic"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`DynamicOptic` is a runtime path through nested data structures in ZIO Blocks. It is the untyped,
|
|
7
|
+
dynamically-constructed counterpart to the typed [`Optic[S, A]`](./optics.md). Where typed optics are bound to specific
|
|
8
|
+
Scala types at compile time, a `DynamicOptic` is a sequence of **navigation steps** that can be built, composed, and
|
|
9
|
+
applied at runtime:
|
|
10
|
+
|
|
11
|
+
```scala
|
|
12
|
+
import zio.blocks.schema._
|
|
13
|
+
|
|
14
|
+
// Build a path: .users[0].name
|
|
15
|
+
val path = DynamicOptic.root.field("users").at(0).field("name")
|
|
16
|
+
|
|
17
|
+
// Navigate a DynamicValue
|
|
18
|
+
val data = DynamicValue.Record(
|
|
19
|
+
"users" -> DynamicValue.Sequence(
|
|
20
|
+
DynamicValue.Record("name" -> DynamicValue.string("Alice"))
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
val result = data.get(path).one
|
|
25
|
+
// Right(DynamicValue.Primitive(PrimitiveValue.String("Alice")))
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Motivation
|
|
29
|
+
|
|
30
|
+
Most of the time you work with typed `Optic[S, A]` values — they are statically verified and provide type-safe
|
|
31
|
+
get/set/modify operations. `DynamicOptic` is useful when:
|
|
32
|
+
|
|
33
|
+
- **The path is determined at runtime** — user input, configuration, query parameters
|
|
34
|
+
- **You are working with `DynamicValue`** — navigating, modifying, or querying schema-less data
|
|
35
|
+
- **You need schema introspection** — walking a `Schema` or `Reflect` structure programmatically
|
|
36
|
+
- **Serializing optic paths** — `DynamicOptic` has its own `Schema[DynamicOptic]` and can be persisted
|
|
37
|
+
|
|
38
|
+
The relationship between typed and dynamic optics:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
Typed World Dynamic World
|
|
42
|
+
┌──────────────────┐ ┌──────────────────┐
|
|
43
|
+
│ Optic[S, A] │─────────────│ DynamicOptic │
|
|
44
|
+
│ │ toDynamic │ │
|
|
45
|
+
│ Lens, Prism, │ │ Sequence of │
|
|
46
|
+
│ Optional, │ │ Node steps │
|
|
47
|
+
│ Traversal │ │ │
|
|
48
|
+
├──────────────────┤ ├──────────────────┤
|
|
49
|
+
│ Operates on │ │ Operates on │
|
|
50
|
+
│ typed values │ │ DynamicValue, │
|
|
51
|
+
│ (case classes, │ │ Schema, │
|
|
52
|
+
│ sealed traits) │ │ Reflect │
|
|
53
|
+
└──────────────────┘ └──────────────────┘
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The `Optic[S, A]` and `DynamicOptic` types serve complementary roles in ZIO Blocks' optics system. `Optic[S, A]` provides compile-time type safety through macros or manual construction, operates on typed Scala values, and can be converted to a `DynamicOptic` via the `optic.toDynamic` method. In contrast, `DynamicOptic` performs runtime type checking and is constructed through a builder API or [path interpolator](../path-interpolator.md), operating directly on [DynamicValue](./dynamic-value.md), [Schema](./schema.md), and [Reflect](./reflect.md) representations.
|
|
57
|
+
|
|
58
|
+
## Design & Structure
|
|
59
|
+
|
|
60
|
+
`DynamicOptic` is modeled as a case class wrapping an `IndexedSeq[Node]`, where each `Node` represents one step in a navigation path:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
DynamicOptic(nodes: IndexedSeq[Node])
|
|
64
|
+
|
|
65
|
+
sealed trait DynamicOptic.Node
|
|
66
|
+
├── Field(name: String) — named field in a record
|
|
67
|
+
├── Case(name: String) — specific case in a variant
|
|
68
|
+
├── AtIndex(index: Int) — element at index in a sequence
|
|
69
|
+
├── AtIndices(index: Seq[Int]) — elements at multiple indices
|
|
70
|
+
├── AtMapKey(key: DynamicValue) — value at a specific map key
|
|
71
|
+
├── AtMapKeys(keys: Seq[DynamicValue]) — values at multiple map keys
|
|
72
|
+
├── Elements — all elements in a sequence (wildcard)
|
|
73
|
+
├── MapKeys — all keys in a map (wildcard)
|
|
74
|
+
├── MapValues — all values in a map (wildcard)
|
|
75
|
+
└── Wrapped — inner value of a wrapper/newtype
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Each of the `DynamicOptic` nodes represents a different way to navigate through a data structure. Here's a reference table for the nodes, their navigation semantics, and string representations:
|
|
79
|
+
|
|
80
|
+
| Node | Navigates | `toString` | `toScalaString` |
|
|
81
|
+
|-----------------------|-----------------------|--------------|---------------------|
|
|
82
|
+
| `Field("name")` | Record field | `.name` | `.name` |
|
|
83
|
+
| `Case("Email")` | Variant case | `<Email>` | `.when[Email]` |
|
|
84
|
+
| `AtIndex(0)` | Sequence element | `[0]` | `.at(0)` |
|
|
85
|
+
| `AtIndices(Seq(0,2))` | Multiple elements | `[0,2]` | `.atIndices(0, 2)` |
|
|
86
|
+
| `AtMapKey(k)` | Map entry by key | `{"host"}` | `.atKey("host")` |
|
|
87
|
+
| `AtMapKeys(ks)` | Multiple entries | `{"a", "b"}` | `.atKeys("a", "b")` |
|
|
88
|
+
| `Elements` | All sequence elements | `[*]` | `.each` |
|
|
89
|
+
| `MapKeys` | All map keys | `{*:}` | `.eachKey` |
|
|
90
|
+
| `MapValues` | All map values | `{*}` | `.eachValue` |
|
|
91
|
+
| `Wrapped` | Wrapper inner value | `.~` | `.wrapped` |
|
|
92
|
+
|
|
93
|
+
Key design decisions:
|
|
94
|
+
|
|
95
|
+
- **Map keys as `DynamicValue`** — Map keys can be any type (`String`, `Int`, `Boolean`, etc.), so `AtMapKey` stores the key as a `DynamicValue` to remain type-agnostic.
|
|
96
|
+
- **Dual rendering** — `toString` produces a compact interpolator syntax (`.field[0]{key}`), while `toScalaString` produces Scala method-call syntax (`.field.at(0).atKey(key)`). The compact format is designed to be copy-pasteable into the `p"..."` string interpolator, while the Scala format is used in error messages.
|
|
97
|
+
- **Every typed `Optic` has a `toDynamic` method** — This bridges the typed and untyped worlds, allowing any `Lens`, `Prism`, `Optional`, or `Traversal` to produce its `DynamicOptic` representation.
|
|
98
|
+
|
|
99
|
+
## Construction
|
|
100
|
+
|
|
101
|
+
### Starting Points
|
|
102
|
+
|
|
103
|
+
Every `DynamicOptic` starts from `root` (the identity/empty path) or one of the pre-built singletons:
|
|
104
|
+
|
|
105
|
+
```scala
|
|
106
|
+
import zio.blocks.schema.DynamicOptic
|
|
107
|
+
|
|
108
|
+
val root = DynamicOptic.root // empty path: "."
|
|
109
|
+
val elems = DynamicOptic.elements // "[*]"
|
|
110
|
+
val keys = DynamicOptic.mapKeys // "{*:}"
|
|
111
|
+
val values = DynamicOptic.mapValues // "{*}"
|
|
112
|
+
val inner = DynamicOptic.wrapped // ".~"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Builder Methods
|
|
116
|
+
|
|
117
|
+
Chain builder methods on `DynamicOptic.root` (or any existing optic) to construct paths fluently:
|
|
118
|
+
|
|
119
|
+
```scala
|
|
120
|
+
import zio.blocks.schema._
|
|
121
|
+
|
|
122
|
+
// Navigate into a record field, then a sequence index, then another field
|
|
123
|
+
val path = DynamicOptic.root.field("users").at(0).field("name")
|
|
124
|
+
// toString: .users[0].name
|
|
125
|
+
|
|
126
|
+
// Navigate into a map with a typed key
|
|
127
|
+
val configPath = DynamicOptic.root.field("config").atKey("host")
|
|
128
|
+
// toString: .config{"host"}
|
|
129
|
+
|
|
130
|
+
// Navigate into a variant case, then a field
|
|
131
|
+
val resultPath = DynamicOptic.root.field("result").caseOf("Success").field("value")
|
|
132
|
+
// toString: .result<Success>.value
|
|
133
|
+
|
|
134
|
+
// Select all elements, then a field on each
|
|
135
|
+
val allEmails = DynamicOptic.root.field("users").elements.field("email")
|
|
136
|
+
// toString: .users[*].email
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
| Method | Node Produced | Example |
|
|
140
|
+
|------------------|----------------------|-----------------------|
|
|
141
|
+
| `.field(name)` | `Field(name)` | `.field("street")` |
|
|
142
|
+
| `.caseOf(name)` | `Case(name)` | `.caseOf("Email")` |
|
|
143
|
+
| `.at(index)` | `AtIndex(index)` | `.at(0)` |
|
|
144
|
+
| `.atIndices(i*)` | `AtIndices(indices)` | `.atIndices(0, 2, 5)` |
|
|
145
|
+
| `.atKey[K](key)` | `AtMapKey(dv)` | `.atKey("host")` |
|
|
146
|
+
| `.atKeys[K](k*)` | `AtMapKeys(dvs)` | `.atKeys("a", "b")` |
|
|
147
|
+
| `.elements` | `Elements` | `.elements` |
|
|
148
|
+
| `.mapKeys` | `MapKeys` | `.mapKeys` |
|
|
149
|
+
| `.mapValues` | `MapValues` | `.mapValues` |
|
|
150
|
+
| `.wrapped` | `Wrapped` | `.wrapped` |
|
|
151
|
+
|
|
152
|
+
Note: `.atKey` and `.atKeys` require an implicit `Schema[K]` to convert the typed key to a `DynamicValue`.
|
|
153
|
+
|
|
154
|
+
### Path Interpolator
|
|
155
|
+
|
|
156
|
+
The [`p"..."` path interpolator](../path-interpolator.md) provides a concise compile-time syntax for building
|
|
157
|
+
`DynamicOptic` values:
|
|
158
|
+
|
|
159
|
+
```scala
|
|
160
|
+
import zio.blocks.schema._
|
|
161
|
+
|
|
162
|
+
// Equivalent builder vs interpolator
|
|
163
|
+
val builderPath : DynamicOptic = DynamicOptic.root.field("users").at(0).field("name")
|
|
164
|
+
val interpolatorPath: DynamicOptic = p".users[0].name"
|
|
165
|
+
// Both produce the same DynamicOptic
|
|
166
|
+
|
|
167
|
+
// Wildcards
|
|
168
|
+
val allEmails = p".users[*].email"
|
|
169
|
+
|
|
170
|
+
// Map access
|
|
171
|
+
val host = p""".config{"host"}"""
|
|
172
|
+
|
|
173
|
+
// Variant cases
|
|
174
|
+
val success = p".result<Success>.value"
|
|
175
|
+
|
|
176
|
+
// Complex path
|
|
177
|
+
val complex = p""".groups[*].members[0].contacts{"email"}"""
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
See [Path Interpolator](../path-interpolator.md) for the full syntax reference.
|
|
181
|
+
|
|
182
|
+
### From Typed Optics
|
|
183
|
+
|
|
184
|
+
Every typed `Optic[S, A]` can be converted to a `DynamicOptic` via `toDynamic`:
|
|
185
|
+
|
|
186
|
+
```scala
|
|
187
|
+
import zio.blocks.schema._
|
|
188
|
+
|
|
189
|
+
case class Address(street: String, city: String)
|
|
190
|
+
|
|
191
|
+
object Address {
|
|
192
|
+
implicit val schema: Schema[Address] = Schema.derived
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
case class Person(name: String, address: Address)
|
|
196
|
+
|
|
197
|
+
object Person extends CompanionOptics[Person] {
|
|
198
|
+
implicit val schema: Schema[Person] = Schema.derived
|
|
199
|
+
val name: Lens[Person, String] = optic(_.name)
|
|
200
|
+
val street: Lens[Person, String] = optic(_.address.street)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
val dynamicName: DynamicOptic = Person.name.toDynamic
|
|
204
|
+
// toString: .name
|
|
205
|
+
|
|
206
|
+
val dynamicStreet: DynamicOptic = Person.street.toDynamic
|
|
207
|
+
// toString: .address.street
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
See [Optics](./optics.md) for more on typed optics.
|
|
211
|
+
|
|
212
|
+
## Operations
|
|
213
|
+
|
|
214
|
+
### Composition
|
|
215
|
+
|
|
216
|
+
`DynamicOptic` values compose via the `apply` method, which concatenates their node sequences:
|
|
217
|
+
|
|
218
|
+
```scala
|
|
219
|
+
import zio.blocks.schema._
|
|
220
|
+
|
|
221
|
+
val users = DynamicOptic.root.field("users")
|
|
222
|
+
val first = DynamicOptic.root.at(0)
|
|
223
|
+
val name = DynamicOptic.root.field("name")
|
|
224
|
+
|
|
225
|
+
// Compose three paths into one
|
|
226
|
+
val fullPath = users(first)(name)
|
|
227
|
+
// toString: .users[0].name
|
|
228
|
+
|
|
229
|
+
// Compose with pre-built singletons
|
|
230
|
+
val allUserNames = users(DynamicOptic.elements)(name)
|
|
231
|
+
// toString: .users[*].name
|
|
232
|
+
|
|
233
|
+
// Compose with interpolator-built paths
|
|
234
|
+
val emails = users(p"[*].email")
|
|
235
|
+
// toString: .users[*].email
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### DynamicValue Operations
|
|
239
|
+
|
|
240
|
+
`DynamicOptic` is the path argument for all `DynamicValue` operations: `get(path)` for retrieval (with `.one` or `.toChunk`), `modify(path)(f)` for transformation, `set(path, value)` for replacement, `delete(path)` for removal, and `insert(path, value)` for addition. By default, the mutating operations (`modify`, `set`, `delete`, `insert`) are lenient and return the original value unchanged if the path doesn't resolve, whereas `get(path)` yields a failing `DynamicValueSelection` on a missing path (though calling `.toChunk` on it will produce an empty chunk). Each operation also has a strict `*OrFail` variant returning `Either[SchemaError, DynamicValue]` with error details on failure.
|
|
241
|
+
|
|
242
|
+
For example, the `DynamicValue#get` method uses `DynamicOptic` to navigate and extract values:
|
|
243
|
+
|
|
244
|
+
```scala
|
|
245
|
+
import zio.blocks.schema._
|
|
246
|
+
|
|
247
|
+
val data = DynamicValue.Record(
|
|
248
|
+
"users" -> DynamicValue.Sequence(
|
|
249
|
+
DynamicValue.Record("name" -> DynamicValue.string("Alice"), "age" -> DynamicValue.int(30)),
|
|
250
|
+
DynamicValue.Record("name" -> DynamicValue.string("Bob"), "age" -> DynamicValue.int(25))
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
// Get a single value
|
|
255
|
+
val firstName = data.get(p".users[0].name").one
|
|
256
|
+
// Right(DynamicValue.Primitive(PrimitiveValue.String("Alice")))
|
|
257
|
+
|
|
258
|
+
// Get all matching values (wildcard)
|
|
259
|
+
val allNames = data.get(p".users[*].name").toChunk
|
|
260
|
+
// Chunk(DynamicValue.string("Alice"), DynamicValue.string("Bob"))
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Schema & Reflect Operations
|
|
264
|
+
|
|
265
|
+
`DynamicOptic` can navigate schema structures, not just values. This is useful for schema introspection and
|
|
266
|
+
metaprogramming.
|
|
267
|
+
|
|
268
|
+
1. The `Schema#get` method takes a `DynamicOptic` path and returns the `Reflect` for the nested component at that path, if it exists. This allows you to programmatically explore the structure of a schema:
|
|
269
|
+
|
|
270
|
+
```scala
|
|
271
|
+
import zio.blocks.schema._
|
|
272
|
+
|
|
273
|
+
case class Address(street: String, city: String)
|
|
274
|
+
|
|
275
|
+
object Address {
|
|
276
|
+
implicit val schema: Schema[Address] = Schema.derived
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
case class Person(name: String, address: Address)
|
|
280
|
+
|
|
281
|
+
object Person {
|
|
282
|
+
implicit val schema: Schema[Person] = Schema.derived
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Get the Reflect for the "street" field inside Person
|
|
286
|
+
val streetReflect: Option[Reflect.Bound[?]] =
|
|
287
|
+
Schema[Person].get(p".address.street")
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
2. The `DynamicSchema#get` method works similarly, allowing you to navigate a `DynamicSchema` structure:
|
|
291
|
+
|
|
292
|
+
```scala
|
|
293
|
+
import zio.blocks.schema._
|
|
294
|
+
|
|
295
|
+
case class Person(name: String, age: Int)
|
|
296
|
+
|
|
297
|
+
object Person {
|
|
298
|
+
implicit val schema: Schema[Person] = Schema.derived
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
val dynSchema = Schema[Person].toDynamicSchema
|
|
302
|
+
|
|
303
|
+
val nameReflect: Option[Reflect.Unbound[_]] =
|
|
304
|
+
dynSchema.get(p".name")
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
3. By applying a `DynamicOptic` directly to a `Reflect` value, you can navigate the reflected structure of a type:
|
|
308
|
+
|
|
309
|
+
```scala
|
|
310
|
+
import zio.blocks.schema._
|
|
311
|
+
|
|
312
|
+
case class Person(name: String, age: Int)
|
|
313
|
+
|
|
314
|
+
object Person {
|
|
315
|
+
implicit val schema: Schema[Person] = Schema.derived
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
val reflect = Schema[Person].reflect
|
|
319
|
+
val path = p".name"
|
|
320
|
+
|
|
321
|
+
// Using the DynamicOptic's apply method
|
|
322
|
+
val result: Option[Reflect[?, ?]] = path(reflect)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## Failure Path in OpticCheck
|
|
326
|
+
|
|
327
|
+
Typed optics use `DynamicOptic` internally for error reporting. When a typed optic operation fails (e.g., a `Prism` encounters the wrong case), the error includes the `DynamicOptic` path to pinpoint exactly where the failure occurred.
|
|
328
|
+
|
|
329
|
+
```
|
|
330
|
+
OpticCheck(errors: ::[Single])
|
|
331
|
+
└── Single (sealed trait)
|
|
332
|
+
├── Error (sealed trait)
|
|
333
|
+
│ ├── UnexpectedCase — prism matched wrong variant case
|
|
334
|
+
│ └── WrappingError — wrapper conversion failed
|
|
335
|
+
└── Warning (sealed trait)
|
|
336
|
+
├── EmptySequence — traversal over empty sequence
|
|
337
|
+
├── SequenceIndexOutOfBounds — index beyond sequence length
|
|
338
|
+
├── MissingKey — map key not found
|
|
339
|
+
└── EmptyMap — traversal over empty map
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Every `OpticCheck.Single` carries two `DynamicOptic` paths:
|
|
343
|
+
|
|
344
|
+
- **`full`** — The complete optic path that was being evaluated
|
|
345
|
+
- **`prefix`** — The path up to the point where the error occurred
|
|
346
|
+
|
|
347
|
+
Error messages use `toScalaString` for human-readable output:
|
|
348
|
+
|
|
349
|
+
```
|
|
350
|
+
During attempted access at .when[Email].subject,
|
|
351
|
+
encountered an unexpected case at .when[Email]:
|
|
352
|
+
expected Email, but got Push
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
## Path String Syntax
|
|
356
|
+
|
|
357
|
+
`DynamicOptic` provides two string formats for different contexts:
|
|
358
|
+
|
|
359
|
+
- **`toString`** — Compact path syntax matching the [`p"..."` interpolator](../path-interpolator.md) format
|
|
360
|
+
- **`toScalaString`** — Scala method call syntax used in error messages
|
|
361
|
+
|
|
362
|
+
| Node | `toString` | `toScalaString` |
|
|
363
|
+
|---------------------------|--------------|---------------------|
|
|
364
|
+
| `Field("name")` | `.name` | `.name` |
|
|
365
|
+
| `Case("Email")` | `<Email>` | `.when[Email]` |
|
|
366
|
+
| `AtIndex(0)` | `[0]` | `.at(0)` |
|
|
367
|
+
| `AtIndices(Seq(0, 2))` | `[0,2]` | `.atIndices(0, 2)` |
|
|
368
|
+
| `AtMapKey(string "host")` | `{"host"}` | `.atKey("host")` |
|
|
369
|
+
| `AtMapKey(int 42)` | `{42}` | `.atKey(42)` |
|
|
370
|
+
| `AtMapKey(bool true)` | `{true}` | `.atKey(true)` |
|
|
371
|
+
| `AtMapKey(char 'a')` | `{'a'}` | `.atKey('a')` |
|
|
372
|
+
| `AtMapKeys(strings)` | `{"a", "b"}` | `.atKeys("a", "b")` |
|
|
373
|
+
| `Elements` | `[*]` | `.each` |
|
|
374
|
+
| `MapKeys` | `{*:}` | `.eachKey` |
|
|
375
|
+
| `MapValues` | `{*}` | `.eachValue` |
|
|
376
|
+
| `Wrapped` | `.~` | `.wrapped` |
|
|
377
|
+
| root | `.` | `.` |
|
|
378
|
+
|
|
379
|
+
## Serialization
|
|
380
|
+
|
|
381
|
+
`DynamicOptic` has an implicit `Schema[DynamicOptic]` defined in its companion object, which means it can be serialized and deserialized just like any other schema-equipped type. This enables storing optic paths in databases, sending them over the wire, or including them in configuration files:
|
|
382
|
+
|
|
383
|
+
```scala
|
|
384
|
+
import zio.blocks.schema._
|
|
385
|
+
|
|
386
|
+
// Schema[DynamicOptic] is available implicitly
|
|
387
|
+
val opticSchema: Schema[DynamicOptic] = Schema[DynamicOptic]
|
|
388
|
+
|
|
389
|
+
// Convert to/from DynamicValue for serialization
|
|
390
|
+
val path = p".users[0].name"
|
|
391
|
+
val serialized: DynamicValue = opticSchema.toDynamicValue(path)
|
|
392
|
+
```
|
package/reference/formats.md
CHANGED
|
@@ -1,11 +1,78 @@
|
|
|
1
1
|
---
|
|
2
2
|
id: formats
|
|
3
3
|
title: "Serialization Formats"
|
|
4
|
+
sidebar_label: "Formats"
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
ZIO Blocks Schema provides automatic codec derivation for multiple serialization formats. Once you have a `Schema[A]` for your data type, you can derive codecs for any supported format using the unified `Schema.derive(Format)` pattern.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
A `Format` is an abstraction that bundles together everything needed to serialize and deserialize data in a specific format (JSON, Avro, Protobuf, etc.).
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
Each format defines the types of input for decoding and output for encoding, as well as the typeclass used as a codec for that format. Each format contains a `Deriver` corresponding to its specific MIME type, which is used to derive codecs from schemas:
|
|
14
|
+
|
|
15
|
+
```scala
|
|
16
|
+
trait Format {
|
|
17
|
+
type DecodeInput
|
|
18
|
+
type EncodeOutput
|
|
19
|
+
type TypeClass[A] <: Codec[DecodeInput, EncodeOutput, A]
|
|
20
|
+
def mimeType: String
|
|
21
|
+
def deriver: Deriver[TypeClass]
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
It unifies all metadata related to serialization formats, such as MIME type and codec deriver, in a single place. This allows for a consistent API across different formats when deriving codecs from schemas. Having MIME type information helps with runtime content negotiation and format routing, for example in HTTP servers or message queues.
|
|
26
|
+
|
|
27
|
+
That is, you can easily call [`Schema[A].derive(format)`](./type-class-derivation.md#using-the-deriver-to-derive-type-class-instances) for any format that implements the `Format` trait, and receive a codec that can encode and decode values of type `A` according to the rules of that format.
|
|
28
|
+
|
|
29
|
+
Formats are categorized into `BinaryFormat` and `TextFormat`, which specify the types of input and output for encoding and decoding:
|
|
30
|
+
|
|
31
|
+
```scala
|
|
32
|
+
sealed trait Format
|
|
33
|
+
abstract class BinaryFormat[...](...) extends Format { ... }
|
|
34
|
+
abstract class TextFormat[...](...) extends Format { ... }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
For example, the `JsonFormat` is a `BinaryFormat` that represents a JSON binary format, where the input for decoding is `ByteBuffer` and the output for encoding is also `ByteBuffer`, the MIME type is `application/json`, and the deriver for generating codecs from schemas is `JsonBinaryCodecDeriver`:
|
|
38
|
+
|
|
39
|
+
```scala
|
|
40
|
+
object JsonFormat extends BinaryFormat("application/json", JsonBinaryCodecDeriver)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Built-in Formats
|
|
44
|
+
|
|
45
|
+
Here's a summary of the formats currently supported by ZIO Blocks. Each format provides a `BinaryFormat` object that can be passed to `derive`:
|
|
46
|
+
|
|
47
|
+
| Format Object | Codec Type | MIME Type | Module |
|
|
48
|
+
|---------------------|-----------------------------|-----------------------|---------------------------------|
|
|
49
|
+
| `JsonFormat` | `JsonBinaryCodec[A]` | `application/json` | `zio-blocks-schema` |
|
|
50
|
+
| `ToonFormat` | `ToonBinaryCodec[A]` | `text/toon` | `zio-blocks-schema-toon` |
|
|
51
|
+
| `MessagePackFormat` | `MessagePackBinaryCodec[A]` | `application/msgpack` | `zio-blocks-schema-messagepack` |
|
|
52
|
+
| `AvroFormat` | `AvroBinaryCodec[A]` | `application/avro` | `zio-blocks-schema-avro` |
|
|
53
|
+
| `ThriftFormat` | `ThriftBinaryCodec[A]` | `application/thrift` | `zio-blocks-schema-thrift` |
|
|
54
|
+
|
|
55
|
+
## Defining a Custom Format
|
|
56
|
+
|
|
57
|
+
To add a new serialization format, define a `BinaryFormat` (or `TextFormat`) singleton with a custom `Deriver`:
|
|
58
|
+
|
|
59
|
+
```scala
|
|
60
|
+
import zio.blocks.schema.codec.{BinaryCodec, BinaryFormat}
|
|
61
|
+
import zio.blocks.schema.derive.Deriver
|
|
62
|
+
|
|
63
|
+
// 1. Define your codec base class
|
|
64
|
+
abstract class MyCodec[A] extends BinaryCodec[A]
|
|
65
|
+
|
|
66
|
+
// 2. Implement a Deriver[MyCodec] (see Type-class Derivation docs)
|
|
67
|
+
// val myDeriver: Deriver[MyCodec] = ...
|
|
68
|
+
|
|
69
|
+
// 3. Create the format singleton
|
|
70
|
+
// object MyFormat extends BinaryFormat[MyCodec]("application/x-myformat", myDeriver)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
For details on implementing a `Deriver`, see [Type-class Derivation](./type-class-derivation.md).
|
|
74
|
+
|
|
75
|
+
## Codec Derivation System
|
|
9
76
|
|
|
10
77
|
All serialization formats in ZIO Blocks follow the same pattern: given a `Schema[A]`, you derive a codec by calling `derive` with a format object:
|
|
11
78
|
|
|
@@ -29,17 +96,6 @@ val bytes: Array[Byte] = codec.encode(Person("Alice", 30))
|
|
|
29
96
|
val result: Either[SchemaError, Person] = codec.decode(bytes)
|
|
30
97
|
```
|
|
31
98
|
|
|
32
|
-
Each format provides a `BinaryFormat` object that can be passed to `derive`:
|
|
33
|
-
|
|
34
|
-
| Format | Object | MIME Type | Platform Support |
|
|
35
|
-
|--------|--------|-----------|------------------|
|
|
36
|
-
| JSON | `JsonFormat` | `application/json` | JVM, JS |
|
|
37
|
-
| TOON | `ToonFormat` | `text/toon` | JVM, JS |
|
|
38
|
-
| MessagePack | `MessagePackFormat` | `application/msgpack` | JVM, JS |
|
|
39
|
-
| Avro | `AvroFormat` | `application/avro` | JVM only |
|
|
40
|
-
| Thrift | `ThriftFormat` | `application/thrift` | JVM only |
|
|
41
|
-
| BSON | `BsonSchemaCodec` | Binary | JVM only |
|
|
42
|
-
|
|
43
99
|
## JSON Format
|
|
44
100
|
|
|
45
101
|
JSON format is the most commonly used text-based serialization format. See the dedicated [JSON documentation](json.md) for comprehensive coverage of the `Json` ADT, navigation, and transformation features.
|
package/reference/json-schema.md
CHANGED
|
@@ -159,6 +159,7 @@ val evenNumber = JsonSchema.integer(
|
|
|
159
159
|
Create schemas for array validation:
|
|
160
160
|
|
|
161
161
|
```scala
|
|
162
|
+
import zio.blocks.chunk.NonEmptyChunk
|
|
162
163
|
import zio.blocks.schema.json.{JsonSchema, JsonSchemaType, NonNegativeInt}
|
|
163
164
|
|
|
164
165
|
// Array of strings
|
|
@@ -181,9 +182,9 @@ val uniqueNumbers = JsonSchema.array(
|
|
|
181
182
|
|
|
182
183
|
// Tuple-like array with prefixItems
|
|
183
184
|
val point2D = JsonSchema.array(
|
|
184
|
-
prefixItems = Some(
|
|
185
|
+
prefixItems = Some(NonEmptyChunk(
|
|
185
186
|
JsonSchema.ofType(JsonSchemaType.Number),
|
|
186
|
-
JsonSchema.ofType(JsonSchemaType.Number)
|
|
187
|
+
JsonSchema.ofType(JsonSchemaType.Number)
|
|
187
188
|
))
|
|
188
189
|
)
|
|
189
190
|
```
|
|
@@ -219,15 +220,17 @@ val strictPerson = JsonSchema.obj(
|
|
|
219
220
|
### Enum and Const
|
|
220
221
|
|
|
221
222
|
```scala
|
|
223
|
+
import zio.blocks.chunk.NonEmptyChunk
|
|
222
224
|
import zio.blocks.schema.json.{JsonSchema, Json}
|
|
223
225
|
|
|
224
226
|
// Enum of string values
|
|
225
|
-
val status = JsonSchema.enumOfStrings(
|
|
227
|
+
val status = JsonSchema.enumOfStrings(NonEmptyChunk("pending", "active", "completed"))
|
|
226
228
|
|
|
227
229
|
// Enum of mixed values
|
|
228
|
-
val mixed = JsonSchema.enumOf(
|
|
230
|
+
val mixed = JsonSchema.enumOf(NonEmptyChunk(
|
|
229
231
|
Json.String("auto"),
|
|
230
|
-
Json.Number(0)
|
|
232
|
+
Json.Number(0),
|
|
233
|
+
Json.Boolean(true)
|
|
231
234
|
))
|
|
232
235
|
|
|
233
236
|
// Constant value
|
|
@@ -467,8 +470,8 @@ Format validation is enabled by default. Use `ValidationOptions.annotationOnly`
|
|
|
467
470
|
JSON Schema 2020-12 introduces `unevaluatedProperties` and `unevaluatedItems` for validating properties/items not matched by any applicator keyword:
|
|
468
471
|
|
|
469
472
|
```scala
|
|
473
|
+
import zio.blocks.chunk.{ChunkMap, NonEmptyChunk}
|
|
470
474
|
import zio.blocks.schema.json.{JsonSchema, JsonSchemaType}
|
|
471
|
-
import zio.blocks.chunk.ChunkMap
|
|
472
475
|
|
|
473
476
|
// Reject any properties not defined in properties or patternProperties
|
|
474
477
|
val strictObject = JsonSchema.Object(
|
|
@@ -480,9 +483,9 @@ val strictObject = JsonSchema.Object(
|
|
|
480
483
|
|
|
481
484
|
// Reject extra array items not matched by prefixItems or items
|
|
482
485
|
val strictArray = JsonSchema.Object(
|
|
483
|
-
prefixItems = Some(
|
|
486
|
+
prefixItems = Some(NonEmptyChunk(
|
|
484
487
|
JsonSchema.ofType(JsonSchemaType.String),
|
|
485
|
-
JsonSchema.ofType(JsonSchemaType.Number)
|
|
488
|
+
JsonSchema.ofType(JsonSchemaType.Number)
|
|
486
489
|
)),
|
|
487
490
|
unevaluatedItems = Some(JsonSchema.False)
|
|
488
491
|
)
|
|
@@ -555,8 +558,8 @@ The implementation passes **817 of 844 tests** (97%+) from the official JSON Sch
|
|
|
555
558
|
## Complete Example
|
|
556
559
|
|
|
557
560
|
```scala
|
|
561
|
+
import zio.blocks.chunk.{ChunkMap, NonEmptyChunk}
|
|
558
562
|
import zio.blocks.schema.json._
|
|
559
|
-
import zio.blocks.chunk.ChunkMap
|
|
560
563
|
|
|
561
564
|
// Define a complex schema
|
|
562
565
|
val userSchema = JsonSchema.obj(
|
|
@@ -573,7 +576,7 @@ val userSchema = JsonSchema.obj(
|
|
|
573
576
|
),
|
|
574
577
|
"roles" -> JsonSchema.array(
|
|
575
578
|
items = Some(JsonSchema.enumOfStrings(
|
|
576
|
-
|
|
579
|
+
NonEmptyChunk("admin", "user", "guest")
|
|
577
580
|
)),
|
|
578
581
|
minItems = Some(NonNegativeInt.literal(1)),
|
|
579
582
|
uniqueItems = Some(true)
|