@zio.dev/zio-blocks 0.0.33 → 0.0.51

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.
Files changed (150) hide show
  1. package/guides/compile-time-resource-safety-with-scope.md +16 -17
  2. package/guides/getting-started-with-mux.md +1507 -0
  3. package/guides/query-dsl-extending.md +161 -102
  4. package/guides/query-dsl-fluent-builder.md +217 -157
  5. package/guides/query-dsl-reified-optics.md +12 -10
  6. package/guides/query-dsl-sql.md +246 -165
  7. package/guides/telemetry-guide.md +1069 -0
  8. package/guides/zio-schema-migration.md +29 -22
  9. package/index.md +292 -50
  10. package/package.json +1 -1
  11. package/plans/config-follow-up-prs.md +188 -0
  12. package/plans/config-pr-assessment-roadmap.md +310 -0
  13. package/reference/MuxDataFlow.jsx +250 -0
  14. package/reference/async.md +651 -0
  15. package/reference/chunk.md +3533 -308
  16. package/reference/codegen/case-class.md +436 -0
  17. package/reference/codegen/emitter-config.md +383 -0
  18. package/reference/codegen/examples.md +664 -0
  19. package/reference/codegen/field.md +316 -0
  20. package/reference/codegen/index.md +317 -0
  21. package/reference/codegen/scala-emitter.md +392 -0
  22. package/reference/codegen/scala-file.md +276 -0
  23. package/reference/codegen/sealed-trait.md +408 -0
  24. package/reference/codegen/type-definition.md +340 -0
  25. package/reference/codegen/type-ref.md +201 -0
  26. package/reference/combinators.md +347 -117
  27. package/reference/config.md +158 -0
  28. package/reference/context.md +4 -4
  29. package/reference/datastar.md +346 -0
  30. package/reference/docs.md +1461 -345
  31. package/reference/endpoint/auth-type.md +146 -0
  32. package/reference/endpoint/endpoint.md +297 -0
  33. package/reference/endpoint/http-codec.md +249 -0
  34. package/reference/endpoint/index.md +825 -0
  35. package/reference/endpoint/path-codec.md +237 -0
  36. package/reference/endpoint/route-pattern.md +196 -0
  37. package/reference/endpoint/route-tree.md +111 -0
  38. package/reference/endpoint/segment-codec.md +212 -0
  39. package/reference/html.md +1120 -0
  40. package/reference/htmx/attribute-values.md +359 -0
  41. package/reference/htmx/hx-encoding.md +111 -0
  42. package/reference/htmx/hx-params.md +204 -0
  43. package/reference/htmx/hx-swap.md +276 -0
  44. package/reference/htmx/hx-sync.md +251 -0
  45. package/reference/htmx/hx-target.md +314 -0
  46. package/reference/htmx/hx-trigger.md +457 -0
  47. package/reference/htmx/hx-url-update.md +239 -0
  48. package/reference/htmx/index.md +855 -0
  49. package/reference/http-model/index.md +47 -0
  50. package/reference/http-model/model.md +1481 -0
  51. package/reference/http-model/schema.md +747 -0
  52. package/reference/maybe.md +826 -0
  53. package/reference/media-type.md +2 -2
  54. package/reference/mux.mdx +823 -0
  55. package/reference/openapi.md +1351 -0
  56. package/reference/resource-management/defer-handle.md +1 -1
  57. package/reference/resource-management/resource.md +31 -2
  58. package/reference/resource-management/scope.md +28 -12
  59. package/reference/resource-management/wire.md +3 -7
  60. package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
  61. package/reference/ringbuffer/MpscDiagram.jsx +618 -0
  62. package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
  63. package/reference/ringbuffer/SpscDiagram.jsx +677 -0
  64. package/reference/ringbuffer/advanced.mdx +109 -0
  65. package/reference/ringbuffer/index.mdx +145 -0
  66. package/reference/ringbuffer/mpmc.mdx +151 -0
  67. package/reference/ringbuffer/mpsc.mdx +132 -0
  68. package/reference/ringbuffer/spmc.mdx +108 -0
  69. package/reference/ringbuffer/spsc.mdx +344 -0
  70. package/reference/{allows.md → schema/allows.md} +4 -4
  71. package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
  72. package/reference/{binding.md → schema/binding.md} +2 -3
  73. package/reference/schema/built-in-codecs/avro.md +451 -0
  74. package/reference/schema/built-in-codecs/bson.md +480 -0
  75. package/reference/schema/built-in-codecs/csv.md +564 -0
  76. package/reference/schema/built-in-codecs/index.md +77 -0
  77. package/reference/schema/built-in-codecs/json/index.md +295 -0
  78. package/reference/schema/built-in-codecs/json/json-config.md +217 -0
  79. package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
  80. package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
  81. package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
  82. package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
  83. package/reference/schema/built-in-codecs/messagepack.md +508 -0
  84. package/reference/schema/built-in-codecs/thrift.md +433 -0
  85. package/reference/schema/built-in-codecs/toon.md +1078 -0
  86. package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
  87. package/reference/schema/built-in-codecs/yaml.md +552 -0
  88. package/reference/{codec.md → schema/codec.md} +10 -10
  89. package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +151 -5
  90. package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
  91. package/reference/schema/format.md +92 -0
  92. package/reference/schema/index.md +50 -0
  93. package/reference/schema/migration.md +297 -0
  94. package/reference/{modifier.md → schema/modifier.md} +58 -7
  95. package/reference/{optics.md → schema/optics.md} +2 -2
  96. package/reference/{patch.md → schema/patch.md} +1 -1
  97. package/{path-interpolator.md → reference/schema/path-interpolator.md} +165 -72
  98. package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
  99. package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
  100. package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
  101. package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
  102. package/reference/{schema.md → schema/schema.md} +12 -0
  103. package/reference/{structural-types.md → schema/structural-types.md} +1 -1
  104. package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
  105. package/reference/smithy.md +533 -0
  106. package/reference/sql/db-codec-deriver.md +71 -0
  107. package/reference/sql/db-codec.md +687 -0
  108. package/reference/sql/db-con.md +271 -0
  109. package/reference/sql/db-connection.md +153 -0
  110. package/reference/sql/db-param-writer.md +77 -0
  111. package/reference/sql/db-param.md +66 -0
  112. package/reference/sql/db-result-reader.md +146 -0
  113. package/reference/sql/db-tx.md +82 -0
  114. package/reference/sql/db-value.md +41 -0
  115. package/reference/sql/ddl.md +85 -0
  116. package/reference/sql/frag.md +254 -0
  117. package/reference/sql/index.md +341 -0
  118. package/reference/sql/repo.md +600 -0
  119. package/reference/sql/sql-dialect.md +73 -0
  120. package/reference/sql/sql-logger.md +62 -0
  121. package/reference/sql/sql-name-mapper.md +70 -0
  122. package/reference/sql/table-metadata.md +134 -0
  123. package/reference/sql/table.md +448 -0
  124. package/reference/sql/transactor-zio.md +399 -0
  125. package/reference/sql/transactor.md +353 -0
  126. package/reference/sql-zio.md +112 -0
  127. package/reference/streams/concurrent-operators.md +106 -0
  128. package/reference/streams/index.md +653 -0
  129. package/reference/streams/pipeline.md +718 -0
  130. package/reference/streams/reader.md +1284 -0
  131. package/reference/streams/scala-2-compatibility.md +55 -0
  132. package/reference/streams/sink.md +1426 -0
  133. package/reference/streams/stream.md +2526 -0
  134. package/reference/streams/writer.md +1045 -0
  135. package/reference/streams/zero-boxing.md +275 -0
  136. package/reference/telemetry.md +693 -0
  137. package/reference/typeid.md +5 -19
  138. package/sidebars.js +238 -43
  139. package/reference/formats.md +0 -694
  140. package/reference/http-model.md +0 -1716
  141. package/reference/streams.md +0 -989
  142. package/ringbuffer.md +0 -249
  143. /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
  144. /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
  145. /package/reference/{lazy.md → schema/lazy.md} +0 -0
  146. /package/reference/{reflect.md → schema/reflect.md} +0 -0
  147. /package/reference/{registers.md → schema/registers.md} +0 -0
  148. /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
  149. /package/reference/{syntax.md → schema/syntax.md} +0 -0
  150. /package/reference/{validation.md → schema/validation.md} +0 -0
@@ -39,13 +39,13 @@ The bidirectional data flow looks like this:
39
39
  `As` is part of `zio-blocks-schema`:
40
40
 
41
41
  ```scala
42
- libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.33"
42
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.51"
43
43
  ```
44
44
 
45
45
  For Scala.js and Scala Native, use `%%%`:
46
46
 
47
47
  ```scala
48
- libraryDependencies += "dev.zio" %%% "zio-blocks-schema" % "0.0.33"
48
+ libraryDependencies += "dev.zio" %%% "zio-blocks-schema" % "0.0.51"
49
49
  ```
50
50
 
51
51
  Supported Scala versions: 2.13.x and 3.x.
@@ -89,7 +89,7 @@ manualAs.from(Long.MaxValue)
89
89
  // SchemaError(
90
90
  // List(
91
91
  // ConversionFailed(
92
- // source = DynamicOptic(IndexedSeq()),
92
+ // source = DynamicOptic(ArraySeq()),
93
93
  // details = "overflow",
94
94
  // cause = None
95
95
  // )
@@ -213,7 +213,7 @@ boxAs.from(LongBox(Long.MaxValue))
213
213
  // SchemaError(
214
214
  // List(
215
215
  // ConversionFailed(
216
- // source = DynamicOptic(IndexedSeq()),
216
+ // source = DynamicOptic(ArraySeq()),
217
217
  // details = "Value 9223372036854775807 is out of range for Int [-2147483648, 2147483647]",
218
218
  // cause = None
219
219
  // )
@@ -240,7 +240,7 @@ trait As[A, B] {
240
240
 
241
241
  ```scala
242
242
  val revAs: As[LongBox, IntBox] = boxAs.reverse
243
- // revAs: As[LongBox, IntBox] = zio.blocks.schema.As$$anon$1@6145661b
243
+ // revAs: As[LongBox, IntBox] = zio.blocks.schema.As$$anon$1@29c128e0
244
244
 
245
245
  revAs.into(LongBox(5L))
246
246
  // res11: Either[SchemaError, IntBox] = Right(IntBox(5))
@@ -302,7 +302,7 @@ We import `As.reverseInto` and use it to obtain the reverse `Into[Int, String]`:
302
302
  import As.reverseInto
303
303
 
304
304
  val intToStr: Into[Int, String] = reverseInto[String, Int]
305
- // intToStr: Into[Int, String] = zio.blocks.schema.AsLowPriorityImplicits$$Lambda$17549/0x00007faac29bf110@e1a6316
305
+ // intToStr: Into[Int, String] = zio.blocks.schema.AsLowPriorityImplicits$$Lambda$19927/0x00007f2996e03990@597c011
306
306
  intToStr.into(42)
307
307
  // res14: Either[SchemaError, String] = Right("42")
308
308
  ```
@@ -387,7 +387,7 @@ numericAs.from(LongModel(Long.MaxValue))
387
387
  // SchemaError(
388
388
  // List(
389
389
  // ConversionFailed(
390
- // source = DynamicOptic(IndexedSeq()),
390
+ // source = DynamicOptic(ArraySeq()),
391
391
  // details = "Value 9223372036854775807 is out of range for Int [-2147483648, 2147483647]",
392
392
  // cause = None
393
393
  // )
@@ -584,4 +584,4 @@ result
584
584
 
585
585
  `As[A, B]` is defined in `zio.blocks.schema` alongside `Into[A, B]`. Because `As` is a subtype of `Into`, the two type classes compose naturally: you can derive an outer `As` from inner `As` instances, or mix `As` and custom `Into` instances when some fields need one-way or custom logic.
586
586
 
587
- For a full reference on one-way conversions and the derivation rules that `As` builds on, see [Into](./into.md).
587
+ For a full reference on one-way conversions and the derivation rules that `As` builds on, see [Into](into.md).
@@ -18,7 +18,7 @@ Schema evolution is the process of changing data structures over time while keep
18
18
 
19
19
  ## `Into[A, B]` — One-Way Conversion
20
20
 
21
- [`Into[A, B]`](./into.md) converts a value of type `A` to `Either[SchemaError, B]`. It is the right choice whenever the migration is asymmetric — for example, when adding a field with a default value, removing a field, or transforming data in a way that cannot be reversed.
21
+ [`Into[A, B]`](into.md) converts a value of type `A` to `Either[SchemaError, B]`. It is the right choice whenever the migration is asymmetric — for example, when adding a field with a default value, removing a field, or transforming data in a way that cannot be reversed.
22
22
 
23
23
  Typical use cases:
24
24
 
@@ -28,7 +28,7 @@ Typical use cases:
28
28
 
29
29
  ## `As[A, B]` — Bidirectional Round-Trip
30
30
 
31
- [`As[A, B]`](./as.md) extends `Into[A, B]` with a `from(b: B): Either[SchemaError, A]` reverse direction. It guarantees that `A → B → A` restores the original value (within the constraints of numeric precision and optional fields). Use `As` when both sides of the conversion must remain in sync.
31
+ [`As[A, B]`](as.md) extends `Into[A, B]` with a `from(b: B): Either[SchemaError, A]` reverse direction. It guarantees that `A → B → A` restores the original value (within the constraints of numeric precision and optional fields). Use `As` when both sides of the conversion must remain in sync.
32
32
 
33
33
  Typical use cases:
34
34
 
@@ -69,13 +69,13 @@ Compare this to a manual implementation:
69
69
  `Into` is part of the `zio-blocks-schema` module:
70
70
 
71
71
  ```scala
72
- libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.33"
72
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.51"
73
73
  ```
74
74
 
75
75
  For Scala.js:
76
76
 
77
77
  ```scala
78
- libraryDependencies += "dev.zio" %%% "zio-blocks-schema" % "0.0.33"
78
+ libraryDependencies += "dev.zio" %%% "zio-blocks-schema" % "0.0.51"
79
79
  ```
80
80
 
81
81
  Supported Scala versions: 2.13.x and 3.x.
@@ -241,7 +241,7 @@ Into[Long, Int].into(Long.MaxValue)
241
241
  // SchemaError(
242
242
  // List(
243
243
  // ConversionFailed(
244
- // source = DynamicOptic(IndexedSeq()),
244
+ // source = DynamicOptic(ArraySeq()),
245
245
  // details = "Value 9223372036854775807 is out of range for Int [-2147483648, 2147483647]",
246
246
  // cause = None
247
247
  // )
@@ -253,7 +253,7 @@ Into[Double, Int].into(3.14)
253
253
  // SchemaError(
254
254
  // List(
255
255
  // ConversionFailed(
256
- // source = DynamicOptic(IndexedSeq()),
256
+ // source = DynamicOptic(ArraySeq()),
257
257
  // details = "Value 3.14 cannot be precisely converted to Int",
258
258
  // cause = None
259
259
  // )
@@ -399,7 +399,7 @@ conv.into(Raw(Long.MaxValue))
399
399
  // SchemaError(
400
400
  // List(
401
401
  // ConversionFailed(
402
- // source = DynamicOptic(IndexedSeq()),
402
+ // source = DynamicOptic(ArraySeq()),
403
403
  // details = "Value 9223372036854775807 is out of range for Int [-2147483648, 2147483647]",
404
404
  // cause = None
405
405
  // )
@@ -623,7 +623,7 @@ validate.into(PersonRaw("Bob", 200))
623
623
  // SchemaError(
624
624
  // List(
625
625
  // ConversionFailed(
626
- // source = DynamicOptic(IndexedSeq()),
626
+ // source = DynamicOptic(ArraySeq()),
627
627
  // details = "Validation failed for field 'age': NonEmptyChunk(200 did not satisfy between(0, 150))",
628
628
  // cause = None
629
629
  // )
@@ -854,7 +854,7 @@ result
854
854
  // SchemaError(
855
855
  // List(
856
856
  // ExpectationMismatch(
857
- // source = DynamicOptic(IndexedSeq()),
857
+ // source = DynamicOptic(ArraySeq()),
858
858
  // expectation = "Expected a record"
859
859
  // )
860
860
  // )
@@ -915,7 +915,7 @@ backToMap
915
915
 
916
916
  ## Related Type: `As[A, B]`
917
917
 
918
- `As[A, B]` extends `Into[A, B]` with a reverse direction, enabling round-trip safe bidirectional conversions. Because `As` must guarantee that `A → B → A` restores the original value, it applies stricter derivation constraints than `Into`. See [As](./as.md) for the full reference.
918
+ `As[A, B]` extends `Into[A, B]` with a reverse direction, enabling round-trip safe bidirectional conversions. Because `As` must guarantee that `A → B → A` restores the original value, it applies stricter derivation constraints than `Into`. See [As](as.md) for the full reference.
919
919
 
920
920
  ## Best Practices
921
921
 
@@ -3,23 +3,29 @@ id: schema-expr
3
3
  title: "SchemaExpr"
4
4
  ---
5
5
 
6
- `SchemaExpr[A, +B]` is a **schema-aware expression** that computes a result of type `B` from an input value of type `A`. The input type `A` must be fully described by a [`Schema`](./schema.md), and the expression is built from [optics](./optics.md), literal values, and operators. The fundamental operations are `eval` and `evalDynamic`.
6
+ `SchemaExpr[A, B]` is a **schema-aware expression** that computes a result of type `B` from an input value of type `A`. It is invariant in both type parameters. The input type `A` must be fully described by a [`Schema`](./schema.md), and the expression is built from [optics](./optics.md), literal values, and operators. The fundamental operations are `eval` and `evalDynamic`.
7
+
8
+ At runtime, `SchemaExpr` is a typed wrapper around `DynamicSchemaExpr`. The typed layer carries the input and output schemas, while the dynamic layer stores the serializable AST. This split is why you will sometimes see both `.dynamic` and `DynamicSchemaExpr` in advanced examples: `SchemaExpr` is the public typed API, and `DynamicSchemaExpr` is the untyped transport/runtime form underneath it.
7
9
 
8
10
  `SchemaExpr`:
9
11
  - represents expressions as a reified AST, enabling introspection and serialization
10
12
  - supports relational (`===`, `>`, `<`, `>=`, `<=`, `!=`), logical (`&&`, `||`, `!`), arithmetic (`+`, `-`, `*`), and string (`concat`, `matches`, `length`) operations
11
13
  - evaluates to `Either[OpticCheck, Seq[B]]`, handling failures and multi-valued results from traversals
12
- - is covariant in `B`, the output type
14
+ - preserves both input and output schemas at the type level
13
15
 
14
16
  ```scala
15
- sealed trait SchemaExpr[A, +B] {
17
+ final case class SchemaExpr[A, B](
18
+ dynamic: DynamicSchemaExpr,
19
+ inputSchema: Schema[A],
20
+ outputSchema: Schema[B]
21
+ ) {
16
22
  def eval(input: A): Either[OpticCheck, Seq[B]]
17
23
  def evalDynamic(input: A): Either[OpticCheck, Seq[DynamicValue]]
18
24
  }
19
25
  ```
20
26
 
21
27
  :::tip
22
- For practical walkthroughs of building with `SchemaExpr`, see [Query DSL Part 1: Expressions](../guides/query-dsl-reified-optics.md), [Part 2: SQL Generation](../guides/query-dsl-sql.md), and [Part 3: Extending the Expression Language](../guides/query-dsl-extending.md).
28
+ For practical walkthroughs of building with `SchemaExpr`, see [Query DSL Part 1: Expressions](../../guides/query-dsl-reified-optics.md), [Part 2: SQL Generation](../../guides/query-dsl-sql.md), and [Part 3: Extending the Expression Language](../../guides/query-dsl-extending.md).
23
29
  :::
24
30
 
25
31
  ## Motivation
@@ -70,13 +76,13 @@ val result: Either[OpticCheck, Seq[Boolean]] = combined.eval(alice)
70
76
  ## Installation
71
77
 
72
78
  ```scala
73
- libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.33"
79
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.51"
74
80
  ```
75
81
 
76
82
  For cross-platform (Scala.js):
77
83
 
78
84
  ```scala
79
- libraryDependencies += "dev.zio" %%% "zio-blocks-schema" % "0.0.33"
85
+ libraryDependencies += "dev.zio" %%% "zio-blocks-schema" % "0.0.51"
80
86
  ```
81
87
 
82
88
  Supported Scala versions: 2.13.x and 3.x.
@@ -85,6 +91,12 @@ Supported Scala versions: 2.13.x and 3.x.
85
91
 
86
92
  `SchemaExpr` instances are typically created through **operator syntax on optics** rather than by constructing AST nodes directly. Each operator on `Optic[S, A]` returns a `SchemaExpr[S, B]`.
87
93
 
94
+ There are three common construction styles:
95
+
96
+ - optic/operator syntax such as `Person.age >= 18`
97
+ - literal expressions such as `SchemaExpr.literal[Person, Int](18)`
98
+ - advanced direct construction when you intentionally need to work with the underlying dynamic AST
99
+
88
100
  ### Via Relational Operators on Optics
89
101
 
90
102
  The comparison operators `===`, `>`, `>=`, `<`, `<=`, and `!=` on `Optic[S, A]` create `SchemaExpr.Relational` nodes. Each operator has two overloads — one comparing against a literal value, and one comparing against another optic:
@@ -204,7 +216,7 @@ val adultOrAlice: SchemaExpr[Person, Boolean] = isAdult || isAlice
204
216
 
205
217
  ### Via Direct AST Construction
206
218
 
207
- For advanced use cases, we can construct `SchemaExpr` nodes directly:
219
+ For advanced use cases, you can construct `SchemaExpr` values directly. In most application code, prefer optic/operator syntax or `SchemaExpr.literal` because they preserve the typed surface and are easier to read.
208
220
 
209
221
  ```scala
210
222
  import zio.blocks.schema._
@@ -217,16 +229,20 @@ object Item extends CompanionOptics[Item] {
217
229
  val price: Lens[Item, Int] = $(_.price)
218
230
  }
219
231
 
220
- // Construct AST nodes directly
221
- val lit: SchemaExpr[Item, Int] = new SchemaExpr.Literal(42, Schema[Int])
222
- val opticExpr: SchemaExpr[Item, Int] = new SchemaExpr.Optic(Item.price)
223
- val comparison: SchemaExpr[Item, Boolean] = new SchemaExpr.Relational(
232
+ // Construct via factory methods
233
+ val lit: SchemaExpr[Item, Int] = SchemaExpr.literal[Item, Int](42)
234
+ val opticExpr: SchemaExpr[Item, Int] = SchemaExpr.optic[Item, Int](Item.price.toDynamic, Item.schema)
235
+ val comparison: SchemaExpr[Item, Boolean] = SchemaExpr.relational(
224
236
  opticExpr,
225
237
  lit,
226
238
  SchemaExpr.RelationalOperator.GreaterThan
227
239
  )
228
240
  ```
229
241
 
242
+ `SchemaExpr.literal` is the normal way to inject constants into an expression tree. It produces a typed `SchemaExpr[S, A]`, while storing the value internally as a `DynamicSchemaExpr.Literal`. That is usually what you want in user code, including migration builders.
243
+
244
+ If you need the raw dynamic form for serialization, transport, or custom interpreters, use `.dynamic` on an existing `SchemaExpr` rather than constructing `DynamicSchemaExpr` directly unless you are working on expression internals.
245
+
230
246
  ## Core Operations
231
247
 
232
248
  ### Evaluation
@@ -236,7 +252,7 @@ val comparison: SchemaExpr[Item, Boolean] = new SchemaExpr.Relational(
236
252
  Evaluates the expression against an input value, returning the typed result. The result is a `Seq[B]` because traversal-based expressions can produce multiple values.
237
253
 
238
254
  ```scala
239
- trait SchemaExpr[A, +B] {
255
+ trait SchemaExprLike[A, B] {
240
256
  def eval(input: A): Either[OpticCheck, Seq[B]]
241
257
  }
242
258
  ```
@@ -272,7 +288,7 @@ When an expression wraps a `Traversal` optic, `eval` returns multiple values —
272
288
  Like `eval`, but converts the result to [`DynamicValue`](./dynamic-value.md) instances. This is useful for serialization or when working with schema-agnostic code.
273
289
 
274
290
  ```scala
275
- trait SchemaExpr[A, +B] {
291
+ trait SchemaExprLike[A, B] {
276
292
  def evalDynamic(input: A): Either[OpticCheck, Seq[DynamicValue]]
277
293
  }
278
294
  ```
@@ -290,7 +306,7 @@ object Person extends CompanionOptics[Person] {
290
306
  val name: Lens[Person, String] = $(_.name)
291
307
  }
292
308
 
293
- val nameExpr = new SchemaExpr.Optic(Person.name)
309
+ val nameExpr = SchemaExpr.optic[Person, String](Person.name.toDynamic, Person.schema)
294
310
  val result = nameExpr.evalDynamic(Person("Alice", 30))
295
311
  // Right(List(DynamicValue.Primitive(PrimitiveValue.String("Alice"))))
296
312
  ```
@@ -301,9 +317,13 @@ val result = nameExpr.evalDynamic(Person("Alice", 30))
301
317
 
302
318
  Combines two boolean-typed expressions with logical AND. Both operands must produce `Boolean` results.
303
319
 
320
+ `&&` and `||` are provided via `SchemaExpr.BooleanOps`, an implicit class in the `SchemaExpr` companion, rather than as direct methods on `SchemaExpr`. This lets downstream libraries supply their own `&&`/`||` overloads (e.g. returning a DDB- or SQL-specific expression type) by bringing in their own implicit class via an explicit import, which takes priority over the companion-scope implicit in Scala 2 and 3.
321
+
304
322
  ```scala
305
- trait SchemaExpr[A, +B] {
306
- def &&[B2](that: SchemaExpr[A, B2])(implicit ev: B <:< Boolean, ev2: B2 =:= Boolean): SchemaExpr[A, Boolean]
323
+ // In SchemaExpr companion:
324
+ implicit final class BooleanOps[A, B](val self: SchemaExpr[A, B]) extends AnyVal {
325
+ def and(that: SchemaExpr[A, Boolean])(implicit ev: B <:< Boolean): SchemaExpr[A, Boolean]
326
+ def &&(that: SchemaExpr[A, Boolean])(implicit ev: B <:< Boolean): SchemaExpr[A, Boolean]
307
327
  }
308
328
  ```
309
329
 
@@ -331,8 +351,10 @@ val result = isAdultAlice.eval(Person("Alice", 30))
331
351
  Combines two boolean-typed expressions with logical OR.
332
352
 
333
353
  ```scala
334
- trait SchemaExpr[A, +B] {
335
- def ||[B2](that: SchemaExpr[A, B2])(implicit ev: B <:< Boolean, ev2: B2 =:= Boolean): SchemaExpr[A, Boolean]
354
+ // In SchemaExpr companion:
355
+ implicit final class BooleanOps[A, B](val self: SchemaExpr[A, B]) extends AnyVal {
356
+ def or(that: SchemaExpr[A, Boolean])(implicit ev: B <:< Boolean): SchemaExpr[A, Boolean]
357
+ def ||(that: SchemaExpr[A, Boolean])(implicit ev: B <:< Boolean): SchemaExpr[A, Boolean]
336
358
  }
337
359
  ```
338
360
 
@@ -355,50 +377,43 @@ val result = isAdultOrAlice.eval(Person("Alice", 12))
355
377
  // Right(List(true)) — Alice, even though not adult
356
378
  ```
357
379
 
358
- ## Subtypes
380
+ ## Structure
359
381
 
360
- `SchemaExpr` is a sealed trait with a rich set of case classes representing different expression nodes. These form an **expression AST** that can be inspected, serialized, or translated to other query languages.
382
+ `SchemaExpr` is a typed wrapper. The actual expression AST lives in `DynamicSchemaExpr`, which is a sealed trait of serializable expression nodes. `SchemaExpr` adds the input and output schemas needed to convert between typed values and [`DynamicValue`](./dynamic-value.md).
361
383
 
362
- ### Leaf Nodes
384
+ ### `DynamicSchemaExpr` leaf nodes
363
385
 
364
- #### `SchemaExpr.Literal`
386
+ #### `DynamicSchemaExpr.Literal`
365
387
 
366
- A constant value that ignores the input and always produces the same result.
388
+ A constant value represented directly as a `DynamicValue`.
367
389
 
368
390
  ```scala
369
- object SchemaExpr {
370
- case class Literal[S, A](value: A, schema: Schema[A]) extends SchemaExpr[S, A]
391
+ object DynamicSchemaExpr {
392
+ final case class Literal(value: DynamicValue, schema: Schema[_]) extends DynamicSchemaExpr
371
393
  }
372
394
  ```
373
395
 
374
- The `Literal#eval` always returns `Right(Seq(value))` regardless of the input. The `Literal#schema` parameter enables conversion to `DynamicValue` via `evalDynamic`.
396
+ #### `DynamicSchemaExpr.Select`
375
397
 
376
- #### `SchemaExpr.Optic`
377
-
378
- Wraps an [`Optic[A, B]`](./optics.md) to extract values from the input. The behavior depends on the optic type:
398
+ Selects values from the input using a [`DynamicOptic`](./dynamic-optic.md).
379
399
 
380
400
  ```scala
381
- object SchemaExpr {
382
- case class Optic[A, B](optic: zio.blocks.schema.Optic[A, B]) extends SchemaExpr[A, B]
401
+ object DynamicSchemaExpr {
402
+ final case class Select(path: DynamicOptic) extends DynamicSchemaExpr
383
403
  }
384
404
  ```
385
405
 
386
- | Optic Type | `SchemaExpr.Optic#eval` Behavior |
387
- |-------------|---------------------------------------------------------------------------------------|
388
- | `Lens` | Always succeeds with a single value |
389
- | `Prism` | Succeeds if the input matches the expected case; otherwise returns `Left(OpticCheck)` |
390
- | `Optional` | Succeeds if the value is present; otherwise returns `Left(OpticCheck)` |
391
- | `Traversal` | Returns all elements; returns `Left(OpticCheck)` if the collection is empty |
406
+ `SchemaExpr.optic(...)` and optic operator syntax eventually produce `DynamicSchemaExpr.Select` nodes.
392
407
 
393
- ### Unary Operations
408
+ ### Unary operations
394
409
 
395
- #### `SchemaExpr.Not`
410
+ #### `DynamicSchemaExpr.Not`
396
411
 
397
412
  Negates a boolean expression.
398
413
 
399
414
  ```scala
400
- object SchemaExpr {
401
- case class Not[A](expr: SchemaExpr[A, Boolean]) extends UnaryOp[A, Boolean](expr)
415
+ object DynamicSchemaExpr {
416
+ final case class Not(expr: DynamicSchemaExpr) extends DynamicSchemaExpr
402
417
  }
403
418
  ```
404
419
 
@@ -421,21 +436,21 @@ val result = inactive.eval(User(active = true))
421
436
  // Right(List(false))
422
437
  ```
423
438
 
424
- ### Binary Operations
439
+ ### Binary operations
425
440
 
426
- `Relational`, `Logical`, `Arithmetic`, and `StringConcat` extend `BinaryOp[A, B, C]`, which provides `left` and `right` sub-expressions. `StringRegexMatch` and `StringLength` extend `SchemaExpr` directly — see [Other Operations](#other-operations) below.
441
+ `Relational`, `Logical`, `Arithmetic`, `Bitwise`, and the string binary operations are all represented as `DynamicSchemaExpr` nodes holding child expressions.
427
442
 
428
- #### `SchemaExpr.Relational`
443
+ #### `DynamicSchemaExpr.Relational`
429
444
 
430
445
  Compares two expressions using a `RelationalOperator`. Returns a boolean result.
431
446
 
432
447
  ```scala
433
- object SchemaExpr {
434
- case class Relational[A, B](
435
- left: SchemaExpr[A, B],
436
- right: SchemaExpr[A, B],
448
+ object DynamicSchemaExpr {
449
+ final case class Relational(
450
+ left: DynamicSchemaExpr,
451
+ right: DynamicSchemaExpr,
437
452
  operator: RelationalOperator
438
- ) extends SchemaExpr[A, Boolean]
453
+ ) extends DynamicSchemaExpr
439
454
  }
440
455
  ```
441
456
 
@@ -454,17 +469,17 @@ The available `RelationalOperator` values are:
454
469
  Equality and inequality (`===`, `!=`) compare values directly. Ordering operators (`<`, `<=`, `>`, `>=`) compare via `DynamicValue` ordering internally.
455
470
  :::
456
471
 
457
- #### `SchemaExpr.Logical`
472
+ #### `DynamicSchemaExpr.Logical`
458
473
 
459
474
  Combines two boolean expressions with a `LogicalOperator`.
460
475
 
461
476
  ```scala
462
- object SchemaExpr {
463
- case class Logical[A](
464
- left: SchemaExpr[A, Boolean],
465
- right: SchemaExpr[A, Boolean],
477
+ object DynamicSchemaExpr {
478
+ final case class Logical(
479
+ left: DynamicSchemaExpr,
480
+ right: DynamicSchemaExpr,
466
481
  operator: LogicalOperator
467
- ) extends SchemaExpr[A, Boolean]
482
+ ) extends DynamicSchemaExpr
468
483
  }
469
484
  ```
470
485
 
@@ -475,18 +490,18 @@ The available `LogicalOperator` values are:
475
490
  | `And` | `&&` | Logical conjunction |
476
491
  | `Or` | `\|\|` | Logical disjunction |
477
492
 
478
- #### `SchemaExpr.Arithmetic`
493
+ #### `DynamicSchemaExpr.Arithmetic`
479
494
 
480
495
  Performs arithmetic on two numeric expressions using an `ArithmeticOperator`. Requires an `IsNumeric[A]` type class instance.
481
496
 
482
497
  ```scala
483
- object SchemaExpr {
484
- case class Arithmetic[S, A](
485
- left: SchemaExpr[S, A],
486
- right: SchemaExpr[S, A],
498
+ object DynamicSchemaExpr {
499
+ final case class Arithmetic(
500
+ left: DynamicSchemaExpr,
501
+ right: DynamicSchemaExpr,
487
502
  operator: ArithmeticOperator,
488
- isNumeric: IsNumeric[A]
489
- ) extends SchemaExpr[S, A]
503
+ numericType: NumericTypeTag
504
+ ) extends DynamicSchemaExpr
490
505
  }
491
506
  ```
492
507
 
@@ -498,112 +513,29 @@ The available `ArithmeticOperator` values are:
498
513
  | `Subtract` | `-` | Subtraction |
499
514
  | `Multiply` | `*` | Multiplication |
500
515
 
501
- Supported numeric types via `IsNumeric`: `Byte`, `Short`, `Int`, `Long`, `Float`, `Double`, `BigInt`, `BigDecimal`.
502
-
503
- #### `SchemaExpr.StringConcat`
504
-
505
- Concatenates two string expressions.
506
-
507
- ```scala
508
- object SchemaExpr {
509
- case class StringConcat[A](
510
- left: SchemaExpr[A, String],
511
- right: SchemaExpr[A, String]
512
- ) extends SchemaExpr[A, String]
513
- }
514
- ```
515
-
516
- Created via the `concat` method on string optics:
517
-
518
- ```scala
519
- import zio.blocks.schema._
520
-
521
- case class Greeting(prefix: String)
522
-
523
- object Greeting extends CompanionOptics[Greeting] {
524
- implicit val schema: Schema[Greeting] = Schema.derived
525
-
526
- val prefix: Lens[Greeting, String] = $(_.prefix)
527
- }
528
-
529
- val withName = Greeting.prefix.concat(", World!")
530
- val result = withName.eval(Greeting("Hello"))
531
- // Right(List("Hello, World!"))
532
- ```
533
-
534
- ### Other Operations
535
-
536
- `StringRegexMatch` and `StringLength` extend `SchemaExpr` directly rather than through `UnaryOp` or `BinaryOp`.
537
-
538
- #### `SchemaExpr.StringRegexMatch`
539
-
540
- Tests whether a string matches a regular expression pattern. Despite having two operands (`regex` and `string`), it extends `SchemaExpr[A, Boolean]` directly.
541
-
542
- ```scala
543
- object SchemaExpr {
544
- case class StringRegexMatch[A](
545
- regex: SchemaExpr[A, String],
546
- string: SchemaExpr[A, String]
547
- ) extends SchemaExpr[A, Boolean]
548
- }
549
- ```
550
-
551
- Created via the `matches` method on string optics:
552
-
553
- ```scala
554
- import zio.blocks.schema._
555
-
556
- case class Email(address: String)
557
-
558
- object Email extends CompanionOptics[Email] {
559
- implicit val schema: Schema[Email] = Schema.derived
560
-
561
- val address: Lens[Email, String] = $(_.address)
562
- }
516
+ Supported numeric types include `Byte`, `Short`, `Int`, `Long`, `Float`, `Double`, `BigInt`, and `BigDecimal`.
563
517
 
564
- val isValid = Email.address.matches("^[^@]+@[^@]+\\.[^@]+$")
565
- val result = isValid.eval(Email("alice@example.com"))
566
- // Right(List(true))
567
- ```
518
+ #### Other string and bitwise operations
568
519
 
569
- #### `SchemaExpr.StringLength`
520
+ `DynamicSchemaExpr` also includes:
570
521
 
571
- Computes the length of a string expression. This is a unary operation but extends `SchemaExpr[A, Int]` directly rather than `UnaryOp`.
522
+ - `StringConcat`
523
+ - `StringRegexMatch`
524
+ - `StringLength`
525
+ - `StringSubstring`
526
+ - `StringTrim`
527
+ - `StringToUpperCase`
528
+ - `StringToLowerCase`
529
+ - `StringReplace`
530
+ - `StringStartsWith`
531
+ - `StringEndsWith`
532
+ - `StringContains`
533
+ - `StringIndexOf`
534
+ - `PrimitiveConversion`
535
+ - `Bitwise`
536
+ - `BitwiseNot`
572
537
 
573
- ```scala
574
- object SchemaExpr {
575
- case class StringLength[A](
576
- string: SchemaExpr[A, String]
577
- ) extends SchemaExpr[A, Int]
578
- }
579
- ```
580
-
581
- Created via the `length` method on string optics:
582
-
583
- ```scala
584
- import zio.blocks.schema._
585
-
586
- case class Message(body: String)
587
-
588
- object Message extends CompanionOptics[Message] {
589
- implicit val schema: Schema[Message] = Schema.derived
590
-
591
- val body: Lens[Message, String] = $(_.body)
592
- }
593
-
594
- val bodyLength = Message.body.length
595
- val result = bodyLength.eval(Message("Hello!"))
596
- // Right(List(6))
597
- ```
598
-
599
- ### Abstract Intermediate Traits
600
-
601
- Two sealed traits categorize some expressions by arity:
602
-
603
- - **`UnaryOp[A, B]`** — has a single `expr: SchemaExpr[A, B]`. Extended by `Not`.
604
- - **`BinaryOp[A, B, C]`** — has `left: SchemaExpr[A, B]` and `right: SchemaExpr[A, B]`. Extended by `Relational`, `Logical`, `Arithmetic`, and `StringConcat`.
605
-
606
- Not all expression nodes use these traits — `StringRegexMatch` and `StringLength` extend `SchemaExpr` directly. These intermediate traits are useful for pattern matching when you need to generically process the expression tree.
538
+ These are the cases downstream interpreters should consider when translating `SchemaExpr.dynamic` into SQL, filters, or other query languages.
607
539
 
608
540
  ## Error Handling
609
541
 
@@ -631,19 +563,22 @@ result match {
631
563
 
632
564
  ## Advanced Usage: Building Query DSLs
633
565
 
634
- Because `SchemaExpr` is a sealed, inspectable AST, third-party libraries can pattern-match on the expression tree to translate it into other languages. For example, a database library could translate `SchemaExpr` into SQL:
566
+ Because `SchemaExpr.dynamic` is a sealed, inspectable AST, third-party libraries can translate expressions into other languages. The public entry point should still accept `SchemaExpr`; only the interpreter internals need to cross into `DynamicSchemaExpr`.
635
567
 
636
568
  ```scala
637
- // Pseudocode — illustrates the concept
638
- def toSql[A](expr: SchemaExpr[A, Boolean]): String = expr match {
639
- case SchemaExpr.Relational(left, right, op) =>
569
+ // Pseudocode — keeps SchemaExpr as the public API
570
+ def toSql[A, B](expr: SchemaExpr[A, B]): String =
571
+ toSqlDynamic(expr.dynamic)
572
+
573
+ def toSqlDynamic(expr: DynamicSchemaExpr): String = expr match {
574
+ case DynamicSchemaExpr.Relational(left, right, op) =>
640
575
  s"${toSqlValue(left)} ${opToSql(op)} ${toSqlValue(right)}"
641
- case SchemaExpr.Logical(left, right, SchemaExpr.LogicalOperator.And) =>
642
- s"(${toSql(left)}) AND (${toSql(right)})"
643
- case SchemaExpr.Logical(left, right, SchemaExpr.LogicalOperator.Or) =>
644
- s"(${toSql(left)}) OR (${toSql(right)})"
645
- case SchemaExpr.Not(inner) =>
646
- s"NOT (${toSql(inner)})"
576
+ case DynamicSchemaExpr.Logical(left, right, DynamicSchemaExpr.LogicalOperator.And) =>
577
+ s"(${toSqlDynamic(left)}) AND (${toSqlDynamic(right)})"
578
+ case DynamicSchemaExpr.Logical(left, right, DynamicSchemaExpr.LogicalOperator.Or) =>
579
+ s"(${toSqlDynamic(left)}) OR (${toSqlDynamic(right)})"
580
+ case DynamicSchemaExpr.Not(inner) =>
581
+ s"NOT (${toSqlDynamic(inner)})"
647
582
  // ...
648
583
  }
649
584
  ```
@@ -658,7 +593,7 @@ This is the key advantage of reified expressions over plain functions — the sa
658
593
 
659
594
  ### Schema
660
595
 
661
- [Schema](./schema.md) provides the type information needed by `SchemaExpr.Literal` to convert values to `DynamicValue` via `Literal#evalDynamic`. The `IsNumeric` type class (used by `Arithmetic`) also derives from `Schema`.
596
+ [Schema](./schema.md) provides the type information needed to construct typed `SchemaExpr` values and to convert typed results to and from `DynamicValue` during evaluation.
662
597
 
663
598
  ### DynamicValue
664
599
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  id: schema
3
+ slug: schema
3
4
  title: "Schema"
4
5
  ---
5
6
 
@@ -186,6 +187,17 @@ import zio.blocks.schema.Schema
186
187
  Schema[Option[A]] // Generic option for reference types
187
188
  ```
188
189
 
190
+ ### Either Values
191
+
192
+ An `Either[A, B]` schema is available whenever both branch types have schemas. Primitive values and primitive-backed wrappers use specialized primitive register layouts, while other values use the layouts described by their schemas:
193
+
194
+ ```scala
195
+ import zio.blocks.schema.Schema
196
+
197
+ Schema[Either[String, Int]]
198
+ Schema[Either[Int, Long]]
199
+ ```
200
+
189
201
  ### Collection Types
190
202
 
191
203
  ZIO Blocks also provides polymorphic schemas for standard Scala collections. You can summon schemas for collections of any element type `A` (and key/value types `K`/`V` for maps):
@@ -294,7 +294,7 @@ Structural types integrate seamlessly with ZIO Blocks' broader ecosystem:
294
294
 
295
295
  ### With Schema Evolution Macros
296
296
 
297
- Structural schemas work with [Schema Evolution](./schema-evolution/into.md) macros for cross-type conversion. When two types share the same structural shape, the conversion machinery can work across type boundaries:
297
+ Structural schemas work with [Schema Evolution](schema-evolution/into.md) macros for cross-type conversion. When two types share the same structural shape, the conversion machinery can work across type boundaries:
298
298
 
299
299
  ```scala
300
300
  import zio.blocks.schema.Schema