@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,469 @@
1
+ ---
2
+ id: binding-resolver
3
+ title: "BindingResolver"
4
+ ---
5
+
6
+ `BindingResolver` is the **read-only interface for looking up bindings by type identity** during schema rebinding. Given a type `A`, a resolver searches its internal storage and returns either the matching `Binding` or `None`.
7
+
8
+ ```scala
9
+ trait BindingResolver {
10
+ def resolveRecord[A](implicit typeId: TypeId[A]): Option[Binding.Record[A]]
11
+ def resolveVariant[A](implicit typeId: TypeId[A]): Option[Binding.Variant[A]]
12
+ def resolvePrimitive[A](implicit typeId: TypeId[A]): Option[Binding.Primitive[A]]
13
+ def resolveWrapper[A](implicit typeId: TypeId[A]): Option[Binding.Wrapper[A, _]]
14
+ def resolveDynamic(implicit typeId: TypeId[DynamicValue]): Option[Binding.Dynamic]
15
+ def resolveSeq[X](implicit typeId: TypeId[X], u: UnapplySeq[X]): Option[Binding.Seq[u.C, u.A]]
16
+ def resolveSeqFor[C[_], A](typeId: TypeId[C[A]]): Option[Binding.Seq[C, A]]
17
+ def resolveMap[X](implicit typeId: TypeId[X], u: UnapplyMap[X]): Option[Binding.Map[u.M, u.K, u.V]]
18
+ def resolveMapFor[M[_, _], K, V](typeId: TypeId[M[K, V]]): Option[Binding.Map[M, K, V]]
19
+ final def ++(that: BindingResolver): BindingResolver
20
+ }
21
+ ```
22
+
23
+ `BindingResolver`:
24
+
25
+ - Uses `TypeId` as the lookup key for all resolution methods.
26
+ - Stores sequence and map bindings by their *unapplied type constructor*, so one `List` binding covers `List[Int]`, `List[String]`, and any other element type.
27
+ - Composes via `BindingResolver#++` with left-biased precedence: the left resolver is tried first, the right serves as fallback.
28
+
29
+ ## Motivation
30
+
31
+ ZIO Blocks separates schema **structure** from schema **behavior**:
32
+
33
+ - A `Reflect.Unbound[A]` carries only structural metadata—field names, type names, documentation. It contains no Scala functions and is fully serializable.
34
+ - A `Reflect.Bound[A]` pairs each structural node with its `Binding`, enabling actual construction and deconstruction of values.
35
+
36
+ This separation powers a key workflow: serialize a schema as a `DynamicSchema` (unbound), transmit or store it, then **rebind** it on the other side using a `BindingResolver` to recover a fully operational `Schema[A]`.
37
+
38
+ ```
39
+ Schema[Person]
40
+ │ toDynamicSchema
41
+ ▼
42
+ DynamicSchema ◄── serializable, no Scala functions
43
+ (Reflect.Unbound[_])
44
+ │ rebind[Person](resolver)
45
+ ▼
46
+ Schema[Person] ◄── operational, can encode and decode
47
+ (Reflect.Bound[Person])
48
+ ```
49
+
50
+ The `BindingResolver` is what bridges the final step: it supplies the `Binding` for each node in the unbound reflect tree.
51
+
52
+ A minimal end-to-end example:
53
+
54
+ ```scala
55
+ import zio.blocks.schema._
56
+ import zio.blocks.schema.binding._
57
+
58
+ case class Person(name: String, age: Int)
59
+
60
+ object Person {
61
+ implicit val schema: Schema[Person] = Schema.derived[Person]
62
+ }
63
+
64
+ val dynamic: DynamicSchema = Schema[Person].toDynamicSchema
65
+
66
+ val resolver: BindingResolver =
67
+ BindingResolver.empty.bind(Binding.of[Person]) ++ BindingResolver.defaults
68
+
69
+ val rebound: Schema[Person] = dynamic.rebind[Person](resolver)
70
+ ```
71
+
72
+ ## Predefined Resolvers
73
+
74
+ ZIO Blocks ships three ready-made resolvers. We almost always compose them with `BindingResolver#++` rather than using them in isolation.
75
+
76
+ ### `BindingResolver.empty`
77
+
78
+ `BindingResolver.empty` is an empty `Registry` with no bindings. It is the starting point for building a custom registry with `Registry#bind`:
79
+
80
+ ```scala
81
+ import zio.blocks.schema.binding._
82
+
83
+ val empty: BindingResolver.Registry = BindingResolver.empty
84
+ ```
85
+
86
+ ### `BindingResolver.defaults`
87
+
88
+ `BindingResolver.defaults` is a pre-populated `Registry` covering all primitive types, `java.time` types, `java.util.UUID`, `java.util.Currency`, `DynamicValue`, common sequence types (`List`, `Vector`, `Set`, `IndexedSeq`, `Seq`, `Chunk`), and `Map`. In practice we place it at the right end of a `BindingResolver#++` chain so custom bindings can override it when needed.
89
+
90
+ The types covered by `defaults` include:
91
+
92
+ | Category | Types |
93
+ |-------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
94
+ | Primitives | `Unit`, `Boolean`, `Byte`, `Short`, `Int`, `Long`, `Float`, `Double`, `Char`, `String`, `BigInt`, `BigDecimal` |
95
+ | `java.time` | `DayOfWeek`, `Duration`, `Instant`, `LocalDate`, `LocalDateTime`, `LocalTime`, `Month`, `MonthDay`, `OffsetDateTime`, `OffsetTime`, `Period`, `Year`, `YearMonth`, `ZoneId`, `ZoneOffset`, `ZonedDateTime` |
96
+ | `java.util` | `Currency`, `UUID` |
97
+ | Dynamic | `DynamicValue` |
98
+ | Sequences | `List`, `Vector`, `Set`, `IndexedSeq`, `Seq`, `Chunk` |
99
+ | Maps | `Map` |
100
+
101
+ ```scala
102
+ import zio.blocks.schema.binding._
103
+
104
+ val defaults: BindingResolver.Registry = BindingResolver.defaults
105
+
106
+ defaults.resolvePrimitive[Int] // Some(...)
107
+ defaults.resolvePrimitive[java.time.Instant] // Some(...)
108
+ defaults.resolveSeq[List[Int]] // Some(...)
109
+ defaults.resolveMap[Map[String, Int]] // Some(...)
110
+ ```
111
+
112
+ ### `BindingResolver.reflection` (JVM only)
113
+
114
+ `BindingResolver.reflection` derives `Binding.Record` instances at runtime using Java reflection for case classes. Derived bindings are cached per `TypeId` in a `ConcurrentHashMap`, so the reflection cost is paid only once per type.
115
+
116
+ On Scala.js, `BindingResolver.reflection` is a no-op resolver that returns `None` for every query.
117
+
118
+ ```scala
119
+ import zio.blocks.schema._
120
+ import zio.blocks.schema.binding._
121
+
122
+ case class Order(id: Long, item: String, quantity: Int)
123
+
124
+ object Order {
125
+ implicit val schema: Schema[Order] = Schema.derived[Order]
126
+ }
127
+
128
+ // reflection handles the record; defaults handles Long, String, Int
129
+ val resolver: BindingResolver = BindingResolver.reflection ++ BindingResolver.defaults
130
+
131
+ val rebound: Schema[Order] = Schema[Order].toDynamicSchema.rebind[Order](resolver)
132
+ ```
133
+
134
+ :::warning
135
+ `BindingResolver.reflection` only derives `Binding.Record` for case classes. It returns `None` for primitives, variants, wrappers, sequences, and maps. Always compose it with `BindingResolver.defaults` to cover those types.
136
+ :::
137
+
138
+ ## Building a Registry
139
+
140
+ `BindingResolver.Registry` is an immutable, map-backed resolver. Every `Registry#bind` call returns a **new** `Registry` with the binding added; the original registry is unchanged.
141
+
142
+ ### `Registry#bind` for proper types
143
+
144
+ The unified `Registry#bind` method accepts any proper binding—`Record`, `Variant`, `Primitive`, `Wrapper`, or `Dynamic`—and dispatches to the correct internal storage slot automatically. We typically call `Binding.of[A]` to derive the right binding at compile time:
145
+
146
+ ```scala
147
+ import zio.blocks.schema._
148
+ import zio.blocks.schema.binding._
149
+
150
+ sealed trait Color
151
+ case object Red extends Color
152
+ case object Blue extends Color
153
+
154
+ object Color {
155
+ implicit val schema: Schema[Color] = Schema.derived[Color]
156
+ }
157
+
158
+ case class Palette(primary: Color, name: String)
159
+
160
+ object Palette {
161
+ implicit val schema: Schema[Palette] = Schema.derived[Palette]
162
+ }
163
+
164
+ val registry: BindingResolver.Registry =
165
+ BindingResolver.empty
166
+ .bind(Binding.of[Color]) // derives Binding.Variant[Color]
167
+ .bind(Binding.of[Palette]) // derives Binding.Record[Palette]
168
+ ```
169
+
170
+ `Binding.of[A]` is a compile-time macro that selects the appropriate binding kind based on the type of `A`:
171
+
172
+ - Case class → `Binding.Record`
173
+ - Sealed trait or enum → `Binding.Variant`
174
+ - Scalar type (`Int`, `String`, …) → `Binding.Primitive`
175
+ - Single-field wrapper with a smart constructor → `Binding.Wrapper`
176
+
177
+ :::warning
178
+ Passing a `Binding.Seq` or `Binding.Map` to the unified `Registry#bind` method throws `IllegalArgumentException` at runtime. Use the specialized overloads shown below for collection types.
179
+ :::
180
+
181
+ ### `Registry#bind` for sequence types
182
+
183
+ Sequence bindings are keyed by their unapplied type constructor. We supply `[C[_]]` as the explicit type parameter so the compiler uses the constructor—not a specific applied type—as the lookup key:
184
+
185
+ ```scala
186
+ import zio.blocks.schema.binding._
187
+
188
+ val registry: BindingResolver.Registry =
189
+ BindingResolver.empty.bind[List](Binding.Seq.list[Nothing])
190
+
191
+ // One binding resolves any element type
192
+ registry.resolveSeq[List[Int]] // Some(...)
193
+ registry.resolveSeq[List[String]] // Some(...)
194
+ ```
195
+
196
+ ### `Registry#bind` for map types
197
+
198
+ Map bindings follow the same pattern with `[M[_, _]]` as the type parameter:
199
+
200
+ ```scala
201
+ import zio.blocks.schema.binding._
202
+
203
+ val registry: BindingResolver.Registry =
204
+ BindingResolver.empty.bind[Map](Binding.Map.map[Nothing, Nothing])
205
+
206
+ registry.resolveMap[Map[String, Int]] // Some(...)
207
+ registry.resolveMap[Map[Int, String]] // Some(...)
208
+ ```
209
+
210
+ ## Combining Resolvers
211
+
212
+ The `BindingResolver#++` operator composes two resolvers into a left-biased fallback chain. The left resolver is consulted first; the right is used only when the left returns `None`.
213
+
214
+ ```scala
215
+ trait BindingResolver {
216
+ final def ++(that: BindingResolver): BindingResolver
217
+ }
218
+ ```
219
+
220
+ This makes it straightforward to layer custom bindings over the built-in defaults:
221
+
222
+ ```scala
223
+ import zio.blocks.schema._
224
+ import zio.blocks.schema.binding._
225
+
226
+ case class UserId(value: Long)
227
+
228
+ object UserId {
229
+ implicit val schema: Schema[UserId] =
230
+ Schema[Long].transform(UserId(_), _.value)
231
+ }
232
+
233
+ // Custom bindings shadow matching entries in defaults
234
+ val resolver: BindingResolver =
235
+ BindingResolver.empty.bind(Binding.of[UserId]) ++ BindingResolver.defaults
236
+ ```
237
+
238
+ `BindingResolver#++` is associative for resolution outcomes: `(a ++ b) ++ c` and `a ++ (b ++ c)` always resolve to the same binding for any type.
239
+
240
+ ## Resolution Methods
241
+
242
+ All `resolve*` methods return an `Option` and never throw. They return `None` when no binding is registered for the requested type.
243
+
244
+ ### `BindingResolver#resolveRecord`
245
+
246
+ `BindingResolver#resolveRecord` returns the `Binding.Record` for a product type (case class, tuple, module object):
247
+
248
+ ```scala
249
+ trait BindingResolver {
250
+ def resolveRecord[A](implicit typeId: TypeId[A]): Option[Binding.Record[A]]
251
+ }
252
+ ```
253
+
254
+ The `TypeId[A]` witness is satisfied implicitly; we only need to supply the type parameter:
255
+
256
+ ```scala
257
+ import zio.blocks.schema._
258
+ import zio.blocks.schema.binding._
259
+
260
+ case class Point(x: Double, y: Double)
261
+
262
+ object Point {
263
+ implicit val schema: Schema[Point] = Schema.derived[Point]
264
+ }
265
+
266
+ val registry = BindingResolver.empty.bind(Binding.of[Point])
267
+ val binding: Option[Binding.Record[Point]] = registry.resolveRecord[Point]
268
+ ```
269
+
270
+ ### `BindingResolver#resolveVariant`
271
+
272
+ `BindingResolver#resolveVariant` returns the `Binding.Variant` for a sum type (sealed trait, Scala 3 enum):
273
+
274
+ ```scala
275
+ trait BindingResolver {
276
+ def resolveVariant[A](implicit typeId: TypeId[A]): Option[Binding.Variant[A]]
277
+ }
278
+ ```
279
+
280
+ ```scala
281
+ import zio.blocks.schema._
282
+ import zio.blocks.schema.binding._
283
+
284
+ sealed trait Shape
285
+ case class Circle(radius: Double) extends Shape
286
+ case class Rectangle(w: Double, h: Double) extends Shape
287
+
288
+ object Shape {
289
+ implicit val schema: Schema[Shape] = Schema.derived[Shape]
290
+ }
291
+
292
+ val registry = BindingResolver.empty.bind(Binding.of[Shape])
293
+ val binding: Option[Binding.Variant[Shape]] = registry.resolveVariant[Shape]
294
+ ```
295
+
296
+ ### `BindingResolver#resolvePrimitive`
297
+
298
+ `BindingResolver#resolvePrimitive` returns the `Binding.Primitive` for scalar types such as `Int`, `String`, `java.time.Instant`, and `java.util.UUID`:
299
+
300
+ ```scala
301
+ trait BindingResolver {
302
+ def resolvePrimitive[A](implicit typeId: TypeId[A]): Option[Binding.Primitive[A]]
303
+ }
304
+ ```
305
+
306
+ ```scala
307
+ import zio.blocks.schema.binding._
308
+
309
+ val binding: Option[Binding.Primitive[Int]] = BindingResolver.defaults.resolvePrimitive[Int]
310
+ ```
311
+
312
+ ### `BindingResolver#resolveWrapper`
313
+
314
+ `BindingResolver#resolveWrapper` returns the `Binding.Wrapper` for newtype patterns—single-field case classes and smart-constructor wrappers. The binding holds a `wrap: B => A` and an `unwrap: A => B` function, converting between the inner type `B` and the outer type `A`:
315
+
316
+ ```scala
317
+ trait BindingResolver {
318
+ def resolveWrapper[A](implicit typeId: TypeId[A]): Option[Binding.Wrapper[A, _]]
319
+ }
320
+ ```
321
+
322
+ ```scala
323
+ import zio.blocks.schema._
324
+ import zio.blocks.schema.binding._
325
+
326
+ case class Email(value: String)
327
+
328
+ object Email {
329
+ implicit val schema: Schema[Email] =
330
+ Schema[String].transform(Email(_), _.value)
331
+ }
332
+
333
+ val registry = BindingResolver.empty.bind(Binding.of[Email])
334
+ val binding: Option[Binding.Wrapper[Email, _]] = registry.resolveWrapper[Email]
335
+ ```
336
+
337
+ ### `BindingResolver#resolveDynamic`
338
+
339
+ `BindingResolver#resolveDynamic` returns the `Binding.Dynamic` singleton, used for `DynamicValue` nodes in the reflect tree. `BindingResolver.defaults` already includes it, so manual registration is rarely needed:
340
+
341
+ ```scala
342
+ trait BindingResolver {
343
+ def resolveDynamic(implicit typeId: TypeId[DynamicValue]): Option[Binding.Dynamic]
344
+ }
345
+ ```
346
+
347
+ ```scala
348
+ import zio.blocks.schema.binding._
349
+
350
+ val binding: Option[Binding.Dynamic] = BindingResolver.defaults.resolveDynamic
351
+ ```
352
+
353
+ ### `BindingResolver#resolveSeq` and `BindingResolver#resolveSeqFor`
354
+
355
+ `BindingResolver#resolveSeq` uses `UnapplySeq` evidence to decompose an applied type like `List[Int]` into its constructor `List` and element type `Int`, then looks up the binding by constructor. `BindingResolver#resolveSeqFor` is the explicit variant when the constructor and element types are already known as separate parameters:
356
+
357
+ ```scala
358
+ trait BindingResolver {
359
+ def resolveSeq[X](implicit typeId: TypeId[X], u: UnapplySeq[X]): Option[Binding.Seq[u.C, u.A]]
360
+ def resolveSeqFor[C[_], A](typeId: TypeId[C[A]]): Option[Binding.Seq[C, A]]
361
+ }
362
+ ```
363
+
364
+ Because the binding is stored by type constructor, a single registered `List` binding handles any element type:
365
+
366
+ ```scala
367
+ import zio.blocks.schema.binding._
368
+ import zio.blocks.typeid.TypeId
369
+
370
+ val defaults = BindingResolver.defaults
371
+
372
+ val listIntBinding: Option[Binding.Seq[List, Int]] = defaults.resolveSeq[List[Int]]
373
+ val listStrBinding: Option[Binding.Seq[List, String]] = defaults.resolveSeq[List[String]]
374
+
375
+ // Explicit form using resolveSeqFor
376
+ val explicit: Option[Binding.Seq[List, Int]] =
377
+ defaults.resolveSeqFor[List, Int](TypeId.of[List[Int]])
378
+ ```
379
+
380
+ ### `BindingResolver#resolveMap` and `BindingResolver#resolveMapFor`
381
+
382
+ `BindingResolver#resolveMap` and `BindingResolver#resolveMapFor` follow the same pattern as their sequence counterparts, applied to key-value collection types:
383
+
384
+ ```scala
385
+ trait BindingResolver {
386
+ def resolveMap[X](implicit typeId: TypeId[X], u: UnapplyMap[X]): Option[Binding.Map[u.M, u.K, u.V]]
387
+ def resolveMapFor[M[_, _], K, V](typeId: TypeId[M[K, V]]): Option[Binding.Map[M, K, V]]
388
+ }
389
+ ```
390
+
391
+ ```scala
392
+ import zio.blocks.schema.binding._
393
+
394
+ val binding: Option[Binding.Map[Map, String, Int]] =
395
+ BindingResolver.defaults.resolveMap[Map[String, Int]]
396
+ ```
397
+
398
+ ## Registry Inspection
399
+
400
+ `Registry` exposes several methods to interrogate its state without performing a full resolution. `Registry#contains` checks whether a proper binding (Record, Variant, Primitive, Wrapper, or Dynamic) is registered for type `A`. `Registry#containsSeq` and `Registry#containsMap` check for sequence and map type constructors respectively:
401
+
402
+ ```scala
403
+ final class Registry {
404
+ def contains[A](implicit typeId: TypeId[A]): Boolean
405
+ def containsSeq[X](implicit typeId: TypeId[X], u: UnapplySeq[X]): Boolean
406
+ def containsMap[X](implicit typeId: TypeId[X], u: UnapplyMap[X]): Boolean
407
+ def size: Int
408
+ def isEmpty: Boolean
409
+ def nonEmpty: Boolean
410
+ }
411
+ ```
412
+
413
+ ```scala
414
+ import zio.blocks.schema.binding._
415
+
416
+ val registry = BindingResolver.defaults
417
+
418
+ registry.contains[Int] // true — primitive binding exists
419
+ registry.containsSeq[List[String]] // true — List constructor is bound
420
+ registry.containsMap[Map[Int, Int]] // true — Map constructor is bound
421
+ registry.size // total number of registered bindings
422
+ ```
423
+
424
+ ## Integration with `DynamicSchema`
425
+
426
+ The primary consumer of `BindingResolver` is `DynamicSchema#rebind`. Given an unbound `DynamicSchema`, `DynamicSchema#rebind` walks the `Reflect` tree and queries the resolver for each node's binding, then returns a fully operational `Schema[A]`:
427
+
428
+ ```scala
429
+ import zio.blocks.schema._
430
+ import zio.blocks.schema.binding._
431
+
432
+ case class Product(sku: String, price: Double, tags: List[String])
433
+
434
+ object Product {
435
+ implicit val schema: Schema[Product] = Schema.derived[Product]
436
+ }
437
+
438
+ val dynamic: DynamicSchema = Schema[Product].toDynamicSchema
439
+
440
+ // The resolver must cover every concrete type that appears in the schema tree
441
+ val resolver: BindingResolver =
442
+ BindingResolver.empty.bind(Binding.of[Product]) ++ BindingResolver.defaults
443
+
444
+ val rebound: Schema[Product] = dynamic.rebind[Product](resolver)
445
+ ```
446
+
447
+ When using `BindingResolver.reflection` on the JVM, individual record types do not need to be registered explicitly—the resolver derives their bindings on demand from the class structure:
448
+
449
+ ```scala
450
+ import zio.blocks.schema._
451
+ import zio.blocks.schema.binding._
452
+
453
+ case class Address(street: String, city: String, zip: String)
454
+
455
+ object Address {
456
+ implicit val schema: Schema[Address] = Schema.derived[Address]
457
+ }
458
+
459
+ // No explicit bind(Binding.of[Address]) needed on JVM
460
+ val resolver: BindingResolver = BindingResolver.reflection ++ BindingResolver.defaults
461
+
462
+ val rebound: Schema[Address] = Schema[Address].toDynamicSchema.rebind[Address](resolver)
463
+ ```
464
+
465
+ :::warning
466
+ If `DynamicSchema#rebind` cannot find a binding for any type present in the unbound schema tree, it throws at runtime. Make sure the resolver covers every concrete type—records, variants, wrappers, primitives, and collections—that appears in the schema.
467
+ :::
468
+
469
+ See [Binding](./binding.md) for details on each binding kind, and [Schema](./schema.md) for the overall structure of the schema system.
@@ -339,7 +339,7 @@ When `F[_, _] = NoBinding` in `Reflect[F[_, _], A]` the `Reflect` structure cont
339
339
  1. **Schema serialization**: Convert schemas to JSON Schema or other formats, making them portable
340
340
  2. **Schema rebinding**: Deserialize a schema and rebind it using a `TypeRegistry`, so it becomes type-safe and operational again
341
341
 
342
- We will cover schema serialization and rebinding in more detail in the `Reflect` data type documentation page.
342
+ We will cover schema serialization and rebinding in more detail in the `Reflect` data type documentation page. For the full API of the binding lookup mechanism used during rebinding, see [BindingResolver](./binding-resolver.md).
343
343
 
344
344
  ## Summary
345
345
 
@@ -48,23 +48,23 @@ val result: Either[SchemaError, Person] = Person.codec.decode(bytes)
48
48
  To include the base schema module with JSON support, add the following dependency to your `build.sbt`:
49
49
 
50
50
  ```scala
51
- libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.27"
51
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.28"
52
52
  ```
53
53
 
54
54
  Additional format modules are separate artifacts:
55
55
 
56
56
  ```scala
57
- libraryDependencies += "dev.zio" %% "zio-blocks-schema-avro" % "0.0.27"
58
- libraryDependencies += "dev.zio" %% "zio-blocks-schema-toon" % "0.0.27"
59
- libraryDependencies += "dev.zio" %% "zio-blocks-schema-messagepack" % "0.0.27"
60
- libraryDependencies += "dev.zio" %% "zio-blocks-schema-thrift" % "0.0.27"
61
- libraryDependencies += "dev.zio" %% "zio-blocks-schema-bson" % "0.0.27"
57
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema-avro" % "0.0.28"
58
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema-toon" % "0.0.28"
59
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema-messagepack" % "0.0.28"
60
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema-thrift" % "0.0.28"
61
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema-bson" % "0.0.28"
62
62
  ```
63
63
 
64
64
  For cross-platform projects (Scala.js):
65
65
 
66
66
  ```scala
67
- libraryDependencies += "dev.zio" %%% "zio-blocks-schema" % "0.0.27"
67
+ libraryDependencies += "dev.zio" %%% "zio-blocks-schema" % "0.0.28"
68
68
  ```
69
69
 
70
70
  Supported Scala versions: 2.13.x and 3.x.
package/reference/docs.md CHANGED
@@ -10,7 +10,7 @@ Complete API reference for the zio-blocks-docs module - a zero-dependency GitHub
10
10
  ## Installation
11
11
 
12
12
  ```scala
13
- libraryDependencies += "dev.zio" %% "zio-blocks-docs" % "0.0.27"
13
+ libraryDependencies += "dev.zio" %% "zio-blocks-docs" % "0.0.28"
14
14
  ```
15
15
 
16
16
  ## Core Types
@@ -394,3 +394,8 @@ val opticSchema: Schema[DynamicOptic] = Schema[DynamicOptic]
394
394
  val path = p".users[0].name"
395
395
  val serialized: DynamicValue = opticSchema.toDynamicValue(path)
396
396
  ```
397
+
398
+
399
+ ## See Also
400
+
401
+ - [DynamicSchema](./dynamic-schema.md) — use `DynamicSchema#get` with a `DynamicOptic` to navigate schema trees and inspect nested type structures.