@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.
@@ -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.