@zio.dev/zio-blocks 0.0.27 → 0.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/guides/query-dsl-extending.md +1 -1
- package/guides/query-dsl-fluent-builder.md +1 -1
- package/guides/query-dsl-reified-optics.md +1 -1
- package/guides/query-dsl-sql.md +1 -1
- package/guides/zio-schema-migration.md +6 -6
- package/index.md +18 -12
- package/package.json +1 -1
- package/reference/allows.md +1059 -34
- package/reference/binding-resolver.md +469 -0
- package/reference/binding.md +1 -1
- package/reference/codec.md +7 -7
- package/reference/docs.md +1 -1
- package/reference/dynamic-optic.md +5 -0
- package/reference/dynamic-schema.md +602 -0
- package/reference/dynamic-value.md +5 -0
- package/reference/json-patch.md +803 -0
- package/reference/json.md +1 -1
- package/reference/media-type.md +2 -2
- package/reference/patch.md +4 -0
- package/reference/schema-evolution/as.md +587 -0
- package/reference/schema-evolution/index.md +50 -0
- package/reference/schema-evolution/into.md +1027 -0
- package/reference/schema-expr.md +2 -2
- package/reference/structural-types.md +369 -0
- package/reference/type-class-derivation.md +1 -1
- package/reference/xml.md +606 -45
- package/scope.md +91 -9
- package/sidebars.js +19 -1
- package/reference/schema-evolution.md +0 -540
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: dynamic-schema
|
|
3
|
+
title: "DynamicSchema"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`DynamicSchema` is a **type-erased schema container** that wraps a `Reflect.Unbound[_]` tree — all the structural information from a `Schema[A]` (field names, case names, type identities, validations, annotations) with no Scala functions attached. The two fundamental uses are validating `DynamicValue` instances at runtime, and transporting schemas across process boundaries.
|
|
7
|
+
|
|
8
|
+
```scala
|
|
9
|
+
final case class DynamicSchema(reflect: Reflect.Unbound[_])
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`DynamicSchema`:
|
|
13
|
+
|
|
14
|
+
- Holds the full structural shape of a type without capturing constructors or deconstructors
|
|
15
|
+
- Can validate any `DynamicValue` against its structure using `DynamicSchema#check` and `DynamicSchema#conforms`
|
|
16
|
+
- Can be serialized to a `DynamicValue` and deserialized back, making it storable in databases or transmittable over the network
|
|
17
|
+
- Can be rehydrated into a fully operational `Schema[A]` by supplying runtime bindings via `DynamicSchema#rebind`
|
|
18
|
+
|
|
19
|
+
## Motivation
|
|
20
|
+
|
|
21
|
+
A `Schema[A]` carries both *structure* (field names, type identities, validations) and *behaviour* (constructors and deconstructors as Scala closures). Closures cannot cross process boundaries. `DynamicSchema` is the structural half — a schema stripped of all Scala functions:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
Schema[OrderPlaced] ←── compile-time type, closures attached
|
|
25
|
+
│
|
|
26
|
+
│ Schema[A]#toDynamicSchema
|
|
27
|
+
▼
|
|
28
|
+
DynamicSchema ←── serializable, no closures
|
|
29
|
+
(Reflect.Unbound[_]) stored in registry or sent over the wire
|
|
30
|
+
│
|
|
31
|
+
│ DynamicSchema.toDynamicValue / DynamicSchema.fromDynamicValue
|
|
32
|
+
▼
|
|
33
|
+
DynamicValue ←── uniform, format-neutral blob
|
|
34
|
+
│
|
|
35
|
+
│ DynamicSchema#fromDynamicValue (on the consumer side)
|
|
36
|
+
▼
|
|
37
|
+
DynamicSchema ←── reconstructed from storage
|
|
38
|
+
│
|
|
39
|
+
│ DynamicSchema#rebind[OrderPlaced](resolver)
|
|
40
|
+
▼
|
|
41
|
+
Schema[OrderPlaced] ←── operational again, can encode and decode
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This pattern enables schema registries: the Checkout Service registers its event schema on startup; the Fulfillment Service fetches it and rebinds it against its own type definitions, guaranteeing it uses the exact schema that was in effect when the event was encoded. See [BindingResolver](./binding-resolver.md) for the complete rebinding API.
|
|
45
|
+
|
|
46
|
+
## Creating a DynamicSchema
|
|
47
|
+
|
|
48
|
+
There are two ways to obtain a `DynamicSchema`: strip bindings from an existing typed schema, or reconstruct one from a serialized blob.
|
|
49
|
+
|
|
50
|
+
### `Schema[A]#toDynamicSchema`
|
|
51
|
+
|
|
52
|
+
`Schema[A]#toDynamicSchema` strips all runtime bindings from a typed schema, returning the structural skeleton as a `DynamicSchema`. The method is defined on `Schema`:
|
|
53
|
+
|
|
54
|
+
```scala
|
|
55
|
+
trait Schema[A] {
|
|
56
|
+
def toDynamicSchema: DynamicSchema
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This is the standard entry point. All structural information — field names, case names, type IDs, `Validation` constraints, `Modifier` annotations, docs, default values, and examples — is preserved:
|
|
61
|
+
|
|
62
|
+
```scala
|
|
63
|
+
import zio.blocks.schema._
|
|
64
|
+
|
|
65
|
+
case class Address(street: String, city: String)
|
|
66
|
+
case class Person(name: String, age: Int, address: Address)
|
|
67
|
+
|
|
68
|
+
object Address { implicit val schema: Schema[Address] = Schema.derived[Address] }
|
|
69
|
+
object Person { implicit val schema: Schema[Person] = Schema.derived[Person] }
|
|
70
|
+
|
|
71
|
+
val dynamic: DynamicSchema = Schema[Person].toDynamicSchema
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
We can inspect the schema's structure by printing it:
|
|
75
|
+
|
|
76
|
+
```scala
|
|
77
|
+
dynamic
|
|
78
|
+
// res1: DynamicSchema = DynamicSchema(
|
|
79
|
+
// Record(
|
|
80
|
+
// fields = Vector(
|
|
81
|
+
// Term(
|
|
82
|
+
// name = "name",
|
|
83
|
+
// value = Primitive(
|
|
84
|
+
// primitiveType = String(None),
|
|
85
|
+
// typeId = Impl(
|
|
86
|
+
// name = "String",
|
|
87
|
+
// owner = Owner(List(Package("java"), Package("lang"))),
|
|
88
|
+
// typeParams = List(),
|
|
89
|
+
// typeArgs = List(),
|
|
90
|
+
// defKind = Class(
|
|
91
|
+
// isFinal = true,
|
|
92
|
+
// isAbstract = false,
|
|
93
|
+
// isCase = false,
|
|
94
|
+
// isValue = false,
|
|
95
|
+
// bases = List(
|
|
96
|
+
// Ref(
|
|
97
|
+
// Impl(
|
|
98
|
+
// name = "CharSequence",
|
|
99
|
+
// owner = Owner(List(Package("java"), Package("lang"))),
|
|
100
|
+
// typeParams = List(),
|
|
101
|
+
// typeArgs = List(),
|
|
102
|
+
// defKind = Trait(isSealed = false, bases = List()),
|
|
103
|
+
// selfType = None,
|
|
104
|
+
// aliasedTo = None,
|
|
105
|
+
// representation = None,
|
|
106
|
+
// annotations = List()
|
|
107
|
+
// )
|
|
108
|
+
// ),
|
|
109
|
+
// Ref(
|
|
110
|
+
// Impl(
|
|
111
|
+
// name = "Comparable",
|
|
112
|
+
// owner = Owner(List(Package("java"), Package("lang"))),
|
|
113
|
+
// typeParams = List(
|
|
114
|
+
// TypeParam(
|
|
115
|
+
// name = "T",
|
|
116
|
+
// index = 0,
|
|
117
|
+
// variance = Invariant,
|
|
118
|
+
// bounds = TypeBounds(lower = None, upper = None),
|
|
119
|
+
// kind = Type
|
|
120
|
+
// )
|
|
121
|
+
// ),
|
|
122
|
+
// typeArgs = List(),
|
|
123
|
+
// defKind = Unknown,
|
|
124
|
+
// selfType = None,
|
|
125
|
+
// aliasedTo = None,
|
|
126
|
+
// representation = None,
|
|
127
|
+
// ...
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### `DynamicSchema.fromDynamicValue`
|
|
131
|
+
|
|
132
|
+
`DynamicSchema.fromDynamicValue` reconstructs a `DynamicSchema` from a previously serialized `DynamicValue` blob. This is the consumer-side entry point when schemas are loaded from a registry or database:
|
|
133
|
+
|
|
134
|
+
```scala
|
|
135
|
+
object DynamicSchema {
|
|
136
|
+
def fromDynamicValue(dv: DynamicValue): DynamicSchema
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Example showing the full store/retrieve round-trip:
|
|
141
|
+
|
|
142
|
+
```scala
|
|
143
|
+
import zio.blocks.schema._
|
|
144
|
+
|
|
145
|
+
case class Product(id: Long, name: String, price: Double)
|
|
146
|
+
object Product { implicit val schema: Schema[Product] = Schema.derived[Product] }
|
|
147
|
+
|
|
148
|
+
val original: DynamicSchema = Schema[Product].toDynamicSchema
|
|
149
|
+
val blob: DynamicValue = DynamicSchema.toDynamicValue(original)
|
|
150
|
+
val restored: DynamicSchema = DynamicSchema.fromDynamicValue(blob)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
We inspect the restored schema's type name to confirm the round-trip preserved it:
|
|
154
|
+
|
|
155
|
+
```scala
|
|
156
|
+
restored.typeId.name
|
|
157
|
+
// res3: String = "Product"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Serializing a DynamicSchema
|
|
161
|
+
|
|
162
|
+
`DynamicSchema.toDynamicValue` converts a `DynamicSchema` to a `DynamicValue` for storage or transmission. The result contains only field names, type names, validations, and annotations — no Scala closures:
|
|
163
|
+
|
|
164
|
+
```scala
|
|
165
|
+
object DynamicSchema {
|
|
166
|
+
def toDynamicValue(ds: DynamicSchema): DynamicValue
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The following example converts a simple case class schema and stores the blob:
|
|
171
|
+
|
|
172
|
+
```scala
|
|
173
|
+
import zio.blocks.schema._
|
|
174
|
+
|
|
175
|
+
case class Tag(name: String)
|
|
176
|
+
object Tag { implicit val schema: Schema[Tag] = Schema.derived[Tag] }
|
|
177
|
+
|
|
178
|
+
val ds: DynamicSchema = Schema[Tag].toDynamicSchema
|
|
179
|
+
val blob: DynamicValue = DynamicSchema.toDynamicValue(ds)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
We inspect the blob's value type to confirm the record structure was preserved:
|
|
183
|
+
|
|
184
|
+
```scala
|
|
185
|
+
blob.valueType
|
|
186
|
+
// res5: DynamicValueType = <function1>
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Validating DynamicValues
|
|
190
|
+
|
|
191
|
+
`DynamicSchema` can check whether a `DynamicValue` conforms to its structure. Validation is recursive: field counts and names, variant case names, collection element types, and primitive type + validation constraints are all checked.
|
|
192
|
+
|
|
193
|
+
### `DynamicSchema#check`
|
|
194
|
+
|
|
195
|
+
`DynamicSchema#check` returns `None` if the value is valid, or `Some(SchemaError)` describing the first validation failure:
|
|
196
|
+
|
|
197
|
+
```scala
|
|
198
|
+
final case class DynamicSchema(reflect: Reflect.Unbound[_]) {
|
|
199
|
+
def check(value: DynamicValue): Option[SchemaError]
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The following example shows all three outcomes — a valid value, a missing field, and a type mismatch:
|
|
204
|
+
|
|
205
|
+
```scala
|
|
206
|
+
import zio.blocks.chunk.Chunk
|
|
207
|
+
import zio.blocks.schema._
|
|
208
|
+
|
|
209
|
+
case class Point(x: Int, y: Int)
|
|
210
|
+
object Point { implicit val schema: Schema[Point] = Schema.derived[Point] }
|
|
211
|
+
|
|
212
|
+
val dynSchema = Schema[Point].toDynamicSchema
|
|
213
|
+
|
|
214
|
+
val valid = DynamicValue.Record(Chunk(
|
|
215
|
+
"x" -> DynamicValue.int(3),
|
|
216
|
+
"y" -> DynamicValue.int(7)
|
|
217
|
+
))
|
|
218
|
+
|
|
219
|
+
val missing = DynamicValue.Record(Chunk(
|
|
220
|
+
"x" -> DynamicValue.int(3)
|
|
221
|
+
// "y" missing
|
|
222
|
+
))
|
|
223
|
+
|
|
224
|
+
val wrongType = DynamicValue.Record(Chunk(
|
|
225
|
+
"x" -> DynamicValue.int(3),
|
|
226
|
+
"y" -> DynamicValue.string("not an int")
|
|
227
|
+
))
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
We evaluate all three cases to observe the outcomes:
|
|
231
|
+
|
|
232
|
+
```scala
|
|
233
|
+
dynSchema.check(valid)
|
|
234
|
+
// res7: Option[SchemaError] = None
|
|
235
|
+
dynSchema.check(missing)
|
|
236
|
+
// res8: Option[SchemaError] = Some(
|
|
237
|
+
// SchemaError(
|
|
238
|
+
// List(MissingField(source = DynamicOptic(ArraySeq()), fieldName = "y"))
|
|
239
|
+
// )
|
|
240
|
+
// )
|
|
241
|
+
dynSchema.check(wrongType)
|
|
242
|
+
// res9: Option[SchemaError] = Some(
|
|
243
|
+
// SchemaError(
|
|
244
|
+
// List(
|
|
245
|
+
// ExpectationMismatch(
|
|
246
|
+
// source = DynamicOptic(ArraySeq(Field("y"))),
|
|
247
|
+
// expectation = "Expected Int, got String"
|
|
248
|
+
// )
|
|
249
|
+
// )
|
|
250
|
+
// )
|
|
251
|
+
// )
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Validation rules:
|
|
255
|
+
|
|
256
|
+
- **Records**: every field in the schema must be present; extra fields in the value are rejected.
|
|
257
|
+
- **Variants**: the case name must be valid; the case payload is validated recursively.
|
|
258
|
+
- **Sequences**: every element is validated against the element schema.
|
|
259
|
+
- **Maps**: every key and every value is validated against their respective schemas.
|
|
260
|
+
- **Primitives**: the `PrimitiveType` must match, and any `Validation` constraints (range, pattern, etc.) must pass.
|
|
261
|
+
|
|
262
|
+
### `DynamicSchema#conforms`
|
|
263
|
+
|
|
264
|
+
`DynamicSchema#conforms` is a convenience method that returns `true` when the value is valid:
|
|
265
|
+
|
|
266
|
+
```scala
|
|
267
|
+
final case class DynamicSchema(reflect: Reflect.Unbound[_]) {
|
|
268
|
+
def conforms(value: DynamicValue): Boolean
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
We pass a well-formed record to confirm the return value:
|
|
273
|
+
|
|
274
|
+
```scala
|
|
275
|
+
import zio.blocks.chunk.Chunk
|
|
276
|
+
import zio.blocks.schema._
|
|
277
|
+
|
|
278
|
+
case class Tag(name: String)
|
|
279
|
+
object Tag { implicit val schema: Schema[Tag] = Schema.derived[Tag] }
|
|
280
|
+
|
|
281
|
+
val dynSchema = Schema[Tag].toDynamicSchema
|
|
282
|
+
val value = DynamicValue.Record(Chunk("name" -> DynamicValue.string("scala")))
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
```scala
|
|
286
|
+
dynSchema.conforms(value)
|
|
287
|
+
// res11: Boolean = true
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Rebinding to a Typed Schema
|
|
291
|
+
|
|
292
|
+
`DynamicSchema#rebind` converts a structural `DynamicSchema` back into a fully operational `Schema[A]` by walking the `Reflect.Unbound` tree and attaching runtime bindings from a `BindingResolver`:
|
|
293
|
+
|
|
294
|
+
```scala
|
|
295
|
+
final case class DynamicSchema(reflect: Reflect.Unbound[_]) {
|
|
296
|
+
def rebind[A](resolver: BindingResolver): Schema[A]
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
The resolver must provide a binding for every concrete type referenced in the schema tree: record types, variant types, and wrapper types must be covered explicitly; primitives, sequences, and maps are covered by `BindingResolver.defaults`:
|
|
301
|
+
|
|
302
|
+
```scala
|
|
303
|
+
import zio.blocks.schema._
|
|
304
|
+
import zio.blocks.schema.binding._
|
|
305
|
+
|
|
306
|
+
case class OrderId(value: String)
|
|
307
|
+
case class LineItem(sku: String, quantity: Int)
|
|
308
|
+
case class Order(id: OrderId, items: List[LineItem])
|
|
309
|
+
|
|
310
|
+
object OrderId { implicit val schema: Schema[OrderId] = Schema.derived[OrderId] }
|
|
311
|
+
object LineItem { implicit val schema: Schema[LineItem] = Schema.derived[LineItem] }
|
|
312
|
+
object Order { implicit val schema: Schema[Order] = Schema.derived[Order] }
|
|
313
|
+
|
|
314
|
+
val blob: DynamicValue = DynamicSchema.toDynamicValue(Schema[Order].toDynamicSchema)
|
|
315
|
+
val dynamic: DynamicSchema = DynamicSchema.fromDynamicValue(blob)
|
|
316
|
+
|
|
317
|
+
val resolver: BindingResolver =
|
|
318
|
+
BindingResolver.empty
|
|
319
|
+
.bind(Binding.of[OrderId])
|
|
320
|
+
.bind(Binding.of[LineItem])
|
|
321
|
+
.bind(Binding.of[Order])
|
|
322
|
+
++ BindingResolver.defaults
|
|
323
|
+
|
|
324
|
+
val rebound: Schema[Order] = dynamic.rebind[Order](resolver)
|
|
325
|
+
val order = Order(OrderId("ORD-1"), List(LineItem("SKU-A", 2)))
|
|
326
|
+
val encoded = rebound.toDynamicValue(order)
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
We decode the encoded value to confirm the round-trip is lossless:
|
|
330
|
+
|
|
331
|
+
```scala
|
|
332
|
+
rebound.fromDynamicValue(encoded)
|
|
333
|
+
// res13: Either[SchemaError, Order] = Right(
|
|
334
|
+
// Order(
|
|
335
|
+
// id = OrderId("ORD-1"),
|
|
336
|
+
// items = List(LineItem(sku = "SKU-A", quantity = 2))
|
|
337
|
+
// )
|
|
338
|
+
// )
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
:::warning
|
|
342
|
+
If `DynamicSchema#rebind` cannot find a binding for any type in the unbound schema tree, it throws a `RebindException` at runtime. Ensure the resolver covers every concrete type — records, variants, wrappers, primitives, and standard collections — that appears in the schema. `BindingResolver.defaults` covers all standard primitives, `java.time` types, `List`, `Map`, `Option`, and other standard collections.
|
|
343
|
+
:::
|
|
344
|
+
|
|
345
|
+
See [BindingResolver](./binding-resolver.md) for the full resolver API including `BindingResolver.reflection` for automatic binding via reflection.
|
|
346
|
+
|
|
347
|
+
## Structural Navigation
|
|
348
|
+
|
|
349
|
+
`DynamicSchema#get` navigates into the schema tree using a `DynamicOptic` path, returning the nested `Reflect.Unbound[_]` at that location:
|
|
350
|
+
|
|
351
|
+
```scala
|
|
352
|
+
final case class DynamicSchema(reflect: Reflect.Unbound[_]) {
|
|
353
|
+
def get(optic: DynamicOptic): Option[Reflect.Unbound[_]]
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
The following example navigates to a field nested two levels deep:
|
|
358
|
+
|
|
359
|
+
```scala
|
|
360
|
+
import zio.blocks.schema._
|
|
361
|
+
|
|
362
|
+
case class Address(street: String, city: String)
|
|
363
|
+
case class Person(name: String, age: Int, address: Address)
|
|
364
|
+
|
|
365
|
+
object Address { implicit val schema: Schema[Address] = Schema.derived[Address] }
|
|
366
|
+
object Person { implicit val schema: Schema[Person] = Schema.derived[Person] }
|
|
367
|
+
|
|
368
|
+
val dynSchema = Schema[Person].toDynamicSchema
|
|
369
|
+
val streetSchema: Option[Reflect.Unbound[_]] =
|
|
370
|
+
dynSchema.get(DynamicOptic.root.field("address").field("street"))
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
We map over the result to read the type name at that path:
|
|
374
|
+
|
|
375
|
+
```scala
|
|
376
|
+
streetSchema.map(_.typeId.name)
|
|
377
|
+
// res15: Option[String] = Some("String")
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
See [DynamicOptic](./dynamic-optic.md) for the full path DSL.
|
|
381
|
+
|
|
382
|
+
## Metadata Access and Updates
|
|
383
|
+
|
|
384
|
+
`DynamicSchema` provides read and write access to the metadata stored in the `Reflect` tree.
|
|
385
|
+
|
|
386
|
+
### `DynamicSchema#typeId`
|
|
387
|
+
|
|
388
|
+
Returns the `TypeId` of the root type:
|
|
389
|
+
|
|
390
|
+
```scala
|
|
391
|
+
import zio.blocks.schema._
|
|
392
|
+
|
|
393
|
+
case class Event(id: Long, kind: String)
|
|
394
|
+
object Event { implicit val schema: Schema[Event] = Schema.derived[Event] }
|
|
395
|
+
|
|
396
|
+
val dynSchema = Schema[Event].toDynamicSchema
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
```scala
|
|
400
|
+
dynSchema.typeId.name
|
|
401
|
+
// res17: String = "Event"
|
|
402
|
+
dynSchema.typeId.fullName
|
|
403
|
+
// res18: String = "repl.MdocSession.MdocApp16.Event"
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### `DynamicSchema#doc`
|
|
407
|
+
|
|
408
|
+
Reads and writes the documentation annotation on the schema. The zero-argument form reads the current `Doc`; the single-argument form returns a copy with updated documentation:
|
|
409
|
+
|
|
410
|
+
```scala
|
|
411
|
+
final case class DynamicSchema(reflect: Reflect.Unbound[_]) {
|
|
412
|
+
def doc: Doc
|
|
413
|
+
def doc(value: String): DynamicSchema
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
We attach a description and read it back to confirm it was applied:
|
|
418
|
+
|
|
419
|
+
```scala
|
|
420
|
+
import zio.blocks.schema._
|
|
421
|
+
|
|
422
|
+
case class Event(id: Long, kind: String)
|
|
423
|
+
object Event { implicit val schema: Schema[Event] = Schema.derived[Event] }
|
|
424
|
+
|
|
425
|
+
val updated: DynamicSchema = Schema[Event].toDynamicSchema.doc("An event in the event log")
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
```scala
|
|
429
|
+
updated.doc
|
|
430
|
+
// res20: Doc = Doc(
|
|
431
|
+
// blocks = IndexedSeq(Paragraph(IndexedSeq(Text("An event in the event log")))),
|
|
432
|
+
// metadata = Map()
|
|
433
|
+
// )
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### `DynamicSchema#modifiers` and `DynamicSchema#modifier`
|
|
437
|
+
|
|
438
|
+
`DynamicSchema#modifiers` returns the `Modifier.Reflect` annotations attached to the root node. `DynamicSchema#modifier` returns a copy with an additional modifier appended:
|
|
439
|
+
|
|
440
|
+
```scala
|
|
441
|
+
final case class DynamicSchema(reflect: Reflect.Unbound[_]) {
|
|
442
|
+
def modifiers: Seq[Modifier.Reflect]
|
|
443
|
+
def modifier(m: Modifier.Reflect): DynamicSchema
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### `DynamicSchema#getDefaultValue` and `DynamicSchema#defaultValue`
|
|
448
|
+
|
|
449
|
+
`DynamicSchema#getDefaultValue` returns the stored default `DynamicValue`, if one is set. `DynamicSchema#defaultValue` returns a copy with the given default:
|
|
450
|
+
|
|
451
|
+
```scala
|
|
452
|
+
final case class DynamicSchema(reflect: Reflect.Unbound[_]) {
|
|
453
|
+
def getDefaultValue: Option[DynamicValue]
|
|
454
|
+
def defaultValue(value: DynamicValue): DynamicSchema
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
We attach a default value and then retrieve it:
|
|
459
|
+
|
|
460
|
+
```scala
|
|
461
|
+
import zio.blocks.schema._
|
|
462
|
+
|
|
463
|
+
case class Config(retries: Int)
|
|
464
|
+
object Config { implicit val schema: Schema[Config] = Schema.derived[Config] }
|
|
465
|
+
|
|
466
|
+
val withDefault: DynamicSchema =
|
|
467
|
+
Schema[Config].toDynamicSchema
|
|
468
|
+
.defaultValue(DynamicValue.Record(
|
|
469
|
+
zio.blocks.chunk.Chunk("retries" -> DynamicValue.int(3))
|
|
470
|
+
))
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
```scala
|
|
474
|
+
withDefault.getDefaultValue
|
|
475
|
+
// res22: Option[DynamicValue] = Some(
|
|
476
|
+
// Record(IndexedSeq(("retries", Primitive(Int(3)))))
|
|
477
|
+
// )
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### `DynamicSchema#examples`
|
|
481
|
+
|
|
482
|
+
The zero-argument form returns stored examples as a `Seq[DynamicValue]`. The multi-argument form returns a copy with the given examples set:
|
|
483
|
+
|
|
484
|
+
```scala
|
|
485
|
+
final case class DynamicSchema(reflect: Reflect.Unbound[_]) {
|
|
486
|
+
def examples: Seq[DynamicValue]
|
|
487
|
+
def examples(value: DynamicValue, values: DynamicValue*): DynamicSchema
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
## Converting to a Typed Schema
|
|
492
|
+
|
|
493
|
+
`DynamicSchema#toSchema` returns a `Schema[DynamicValue]` — it stays fully in the dynamic world and requires no bindings. Use it when you have received a `DynamicSchema` over the wire and need a codec-compatible schema that enforces structural conformance without binding any Scala types.
|
|
494
|
+
|
|
495
|
+
After transporting a `DynamicSchema`, you may not have the Scala types that `DynamicSchema#rebind` requires. For example, when the consumer is a validation gateway or format converter that handles arbitrary event shapes, any codec pipeline that accepts `Schema[DynamicValue]` can use the result directly. `DynamicSchema#toSchema` is the right choice for schema validation middleware, event-store gateways, and format converters that must enforce structure without knowing the concrete type.
|
|
496
|
+
|
|
497
|
+
:::warning
|
|
498
|
+
Use `DynamicSchema#rebind` instead when you have a `BindingResolver` and need a fully operational `Schema[A]` for typed encoding and decoding.
|
|
499
|
+
:::
|
|
500
|
+
`DynamicSchema#toSchema` is defined as:
|
|
501
|
+
|
|
502
|
+
```scala
|
|
503
|
+
final case class DynamicSchema(reflect: Reflect.Unbound[_]) {
|
|
504
|
+
def toSchema: Schema[DynamicValue]
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
The following example shows a schema-validation gateway: we receive a `DynamicSchema` from a registry, convert it to a `Schema[DynamicValue]`, and use the result to validate an incoming payload, rejecting it on structural mismatch:
|
|
509
|
+
|
|
510
|
+
```scala
|
|
511
|
+
import zio.blocks.schema._
|
|
512
|
+
|
|
513
|
+
case class OrderEvent(orderId: String, amount: Double)
|
|
514
|
+
object OrderEvent { implicit val schema: Schema[OrderEvent] = Schema.derived[OrderEvent] }
|
|
515
|
+
|
|
516
|
+
val blob: DynamicValue = DynamicSchema.toDynamicValue(Schema[OrderEvent].toDynamicSchema)
|
|
517
|
+
val received: DynamicSchema = DynamicSchema.fromDynamicValue(blob)
|
|
518
|
+
val gatewaySchema: Schema[DynamicValue] = received.toSchema
|
|
519
|
+
|
|
520
|
+
val incoming: DynamicValue = DynamicValue.Record(
|
|
521
|
+
zio.blocks.chunk.Chunk(
|
|
522
|
+
"orderId" -> DynamicValue.string("ORD-42"),
|
|
523
|
+
"amount" -> DynamicValue.double(99.95)
|
|
524
|
+
)
|
|
525
|
+
)
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
We validate the payload and observe the result:
|
|
529
|
+
|
|
530
|
+
```scala
|
|
531
|
+
gatewaySchema.fromDynamicValue(incoming)
|
|
532
|
+
// res24: Either[SchemaError, DynamicValue] = Right(
|
|
533
|
+
// Record(
|
|
534
|
+
// IndexedSeq(
|
|
535
|
+
// ("orderId", Primitive(String("ORD-42"))),
|
|
536
|
+
// ("amount", Primitive(Double(99.95)))
|
|
537
|
+
// )
|
|
538
|
+
// )
|
|
539
|
+
// )
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
## Integration
|
|
543
|
+
|
|
544
|
+
`DynamicSchema` connects to several other ZIO Blocks types, each serving a distinct role in the dynamic layer.
|
|
545
|
+
|
|
546
|
+
### With `DynamicValue`
|
|
547
|
+
|
|
548
|
+
`DynamicSchema` and `DynamicValue` are the two halves of ZIO Blocks' dynamic layer. `DynamicValue` holds runtime data without compile-time types; `DynamicSchema` holds structural metadata without runtime bindings. Together they enable fully type-erased validation and serialization pipelines. See [DynamicValue](./dynamic-value.md).
|
|
549
|
+
|
|
550
|
+
### With `BindingResolver`
|
|
551
|
+
|
|
552
|
+
The primary consumer of `DynamicSchema` in a type-safe context is `DynamicSchema#rebind`, which requires a `BindingResolver` to reattach runtime bindings. See [BindingResolver](./binding-resolver.md) for the complete rebinding API, including `BindingResolver.reflection` for automatic binding discovery.
|
|
553
|
+
|
|
554
|
+
### With `DynamicOptic`
|
|
555
|
+
|
|
556
|
+
`DynamicSchema#get` accepts a `DynamicOptic` path to navigate the structural tree. This allows you to inspect nested schemas, extract type information, or validate sub-trees independently. See [DynamicOptic](./dynamic-optic.md).
|
|
557
|
+
|
|
558
|
+
### With `Schema`
|
|
559
|
+
|
|
560
|
+
`Schema[A]#toDynamicSchema` is defined on `Schema` and is the standard way to obtain a `DynamicSchema`. See [Schema](./schema.md).
|
|
561
|
+
|
|
562
|
+
## Running the Examples
|
|
563
|
+
|
|
564
|
+
All code from this guide is available as runnable examples in the `schema-examples` module.
|
|
565
|
+
|
|
566
|
+
**1. Clone the repository and navigate to the project:**
|
|
567
|
+
|
|
568
|
+
```bash
|
|
569
|
+
git clone https://github.com/zio/zio-blocks.git
|
|
570
|
+
cd zio-blocks
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
**2. Run individual examples with sbt:**
|
|
574
|
+
|
|
575
|
+
**Validate DynamicValues against a DynamicSchema**
|
|
576
|
+
([source](https://github.com/zio/zio-blocks/blob/main/schema-examples/src/main/scala/dynamicschema/DynamicSchemaValidationExample.scala))
|
|
577
|
+
|
|
578
|
+
```bash
|
|
579
|
+
sbt "schema-examples/runMain dynamicschema.DynamicSchemaValidationExample"
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
**Serialize and deserialize a DynamicSchema**
|
|
583
|
+
([source](https://github.com/zio/zio-blocks/blob/main/schema-examples/src/main/scala/dynamicschema/DynamicSchemaSerializationExample.scala))
|
|
584
|
+
|
|
585
|
+
```bash
|
|
586
|
+
sbt "schema-examples/runMain dynamicschema.DynamicSchemaSerializationExample"
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
**Rebind a restored DynamicSchema to a typed Schema**
|
|
590
|
+
([source](https://github.com/zio/zio-blocks/blob/main/schema-examples/src/main/scala/dynamicschema/DynamicSchemaRebindExample.scala))
|
|
591
|
+
|
|
592
|
+
```bash
|
|
593
|
+
sbt "schema-examples/runMain dynamicschema.DynamicSchemaRebindExample"
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
**Complete schema registry pipeline**
|
|
597
|
+
([source](https://github.com/zio/zio-blocks/blob/main/schema-examples/src/main/scala/dynamicschema/DynamicSchemaRegistryExample.scala))
|
|
598
|
+
|
|
599
|
+
```bash
|
|
600
|
+
sbt "schema-examples/runMain dynamicschema.DynamicSchemaRegistryExample"
|
|
601
|
+
```
|
|
602
|
+
|
|
@@ -821,3 +821,8 @@ val new_ = DynamicValue.Record(
|
|
|
821
821
|
val patch: DynamicPatch = old.diff(new_)
|
|
822
822
|
// Patch that updates "age" from 30 to 31
|
|
823
823
|
```
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
## See Also
|
|
827
|
+
|
|
828
|
+
- [DynamicSchema](./dynamic-schema.md) — validate `DynamicValue` instances against a structural schema, or transport schemas as `DynamicValue` blobs.
|