@zio.dev/zio-blocks 0.0.33 → 0.0.55

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 (215) hide show
  1. package/adr/2026-07-18-data-migration.md +123 -0
  2. package/guides/async-getting-started.md +687 -0
  3. package/guides/compile-time-resource-safety-with-scope.md +21 -16
  4. package/guides/getting-started-with-mux.md +1395 -0
  5. package/guides/query-dsl-extending.md +161 -102
  6. package/guides/query-dsl-fluent-builder.md +217 -157
  7. package/guides/query-dsl-reified-optics.md +12 -10
  8. package/guides/query-dsl-sql.md +640 -165
  9. package/guides/sql-checked-interpolation.md +173 -0
  10. package/guides/sql-transactions.md +286 -0
  11. package/guides/telemetry-guide.md +1130 -0
  12. package/guides/zio-schema-migration.md +29 -22
  13. package/index.md +248 -389
  14. package/package.json +1 -1
  15. package/plans/config-follow-up-prs.md +188 -0
  16. package/plans/config-pr-assessment-roadmap.md +310 -0
  17. package/reference/MuxDataFlow.jsx +250 -0
  18. package/reference/async.md +1499 -0
  19. package/reference/chunk.md +3533 -308
  20. package/reference/codegen/case-class.md +436 -0
  21. package/reference/codegen/emitter-config.md +383 -0
  22. package/reference/codegen/examples.md +664 -0
  23. package/reference/codegen/field.md +316 -0
  24. package/reference/codegen/index.md +317 -0
  25. package/reference/codegen/scala-emitter.md +392 -0
  26. package/reference/codegen/scala-file.md +276 -0
  27. package/reference/codegen/sealed-trait.md +408 -0
  28. package/reference/codegen/type-definition.md +340 -0
  29. package/reference/codegen/type-ref.md +201 -0
  30. package/reference/combinators.md +347 -117
  31. package/reference/config/config-decoder.md +460 -0
  32. package/reference/config/config-source.md +489 -0
  33. package/reference/config/errors.md +278 -0
  34. package/reference/config/flags.md +369 -0
  35. package/reference/config/formats.md +314 -0
  36. package/reference/config/index.md +304 -0
  37. package/reference/config/rollout.md +336 -0
  38. package/reference/context.md +9 -52
  39. package/reference/data-migration.md +269 -0
  40. package/reference/datastar/attributes.md +302 -0
  41. package/reference/datastar/events.md +234 -0
  42. package/reference/datastar/index.md +256 -0
  43. package/reference/datastar/signals.md +230 -0
  44. package/reference/datastar/sse.md +295 -0
  45. package/reference/datastar.md +346 -0
  46. package/reference/docs.md +1461 -345
  47. package/reference/endpoint/auth-type.md +146 -0
  48. package/reference/endpoint/bulk-creation.md +96 -0
  49. package/reference/endpoint/endpoint.md +297 -0
  50. package/reference/endpoint/http-codec.md +249 -0
  51. package/reference/endpoint/index.md +745 -0
  52. package/reference/endpoint/path-codec.md +225 -0
  53. package/reference/endpoint/route-pattern.md +194 -0
  54. package/reference/endpoint/route-tree.md +111 -0
  55. package/reference/endpoint/segment-codec.md +199 -0
  56. package/reference/html.md +1424 -0
  57. package/reference/htmx/attribute-values.md +359 -0
  58. package/reference/htmx/hx-encoding.md +111 -0
  59. package/reference/htmx/hx-params.md +204 -0
  60. package/reference/htmx/hx-swap.md +276 -0
  61. package/reference/htmx/hx-sync.md +251 -0
  62. package/reference/htmx/hx-target.md +314 -0
  63. package/reference/htmx/hx-trigger.md +457 -0
  64. package/reference/htmx/hx-url-update.md +239 -0
  65. package/reference/htmx/index.md +807 -0
  66. package/reference/htmx/response-headers.md +240 -0
  67. package/reference/http-model/headers.md +735 -0
  68. package/reference/http-model/index.md +49 -0
  69. package/reference/http-model/model.md +1517 -0
  70. package/reference/http-model/schema-codecs.md +522 -0
  71. package/reference/http-model/schema.md +750 -0
  72. package/reference/http-model/server-sent-event.md +341 -0
  73. package/reference/jwt.md +195 -0
  74. package/reference/maybe.md +943 -0
  75. package/reference/media-type.md +2 -2
  76. package/reference/mux.md +254 -0
  77. package/reference/mux.mdx +828 -0
  78. package/reference/openapi.md +1351 -0
  79. package/reference/projection.md +654 -0
  80. package/reference/resource-management/defer-handle.md +1 -1
  81. package/reference/resource-management/resource.md +31 -98
  82. package/reference/resource-management/scope.md +28 -220
  83. package/reference/resource-management/wire.md +5 -55
  84. package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
  85. package/reference/ringbuffer/MpscDiagram.jsx +618 -0
  86. package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
  87. package/reference/ringbuffer/SpscDiagram.jsx +677 -0
  88. package/reference/ringbuffer/advanced.mdx +109 -0
  89. package/reference/ringbuffer/index.mdx +145 -0
  90. package/reference/ringbuffer/mpmc.mdx +185 -0
  91. package/reference/ringbuffer/mpsc.mdx +164 -0
  92. package/reference/ringbuffer/spmc.mdx +108 -0
  93. package/reference/ringbuffer/spsc.mdx +416 -0
  94. package/reference/{allows.md → schema/allows.md} +4 -100
  95. package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
  96. package/reference/{binding.md → schema/binding.md} +3 -4
  97. package/reference/schema/built-in-codecs/avro.md +451 -0
  98. package/reference/schema/built-in-codecs/bson.md +510 -0
  99. package/reference/schema/built-in-codecs/csv.md +564 -0
  100. package/reference/schema/built-in-codecs/index.md +77 -0
  101. package/reference/schema/built-in-codecs/json/index.md +295 -0
  102. package/reference/schema/built-in-codecs/json/json-config.md +217 -0
  103. package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
  104. package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
  105. package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
  106. package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
  107. package/reference/schema/built-in-codecs/messagepack.md +508 -0
  108. package/reference/schema/built-in-codecs/thrift.md +433 -0
  109. package/reference/schema/built-in-codecs/toon.md +1078 -0
  110. package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
  111. package/reference/schema/built-in-codecs/yaml.md +552 -0
  112. package/reference/{codec.md → schema/codec.md} +11 -11
  113. package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +196 -5
  114. package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
  115. package/reference/schema/format.md +92 -0
  116. package/reference/schema/index.md +52 -0
  117. package/reference/schema/migration.md +297 -0
  118. package/reference/{modifier.md → schema/modifier.md} +58 -7
  119. package/reference/{optics.md → schema/optics.md} +2 -2
  120. package/reference/{patch.md → schema/patch.md} +1 -1
  121. package/{path-interpolator.md → reference/schema/path-interpolator.md} +167 -72
  122. package/reference/schema/reflect-transformer.md +140 -0
  123. package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
  124. package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
  125. package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
  126. package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
  127. package/reference/schema/schema-search.md +263 -0
  128. package/reference/{schema.md → schema/schema.md} +22 -2
  129. package/reference/{structural-types.md → schema/structural-types.md} +1 -1
  130. package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
  131. package/reference/smithy.md +1032 -0
  132. package/reference/sql/db-codec-deriver.md +71 -0
  133. package/reference/sql/db-codec.md +687 -0
  134. package/reference/sql/db-con.md +271 -0
  135. package/reference/sql/db-connection.md +153 -0
  136. package/reference/sql/db-param-writer.md +77 -0
  137. package/reference/sql/db-param.md +66 -0
  138. package/reference/sql/db-result-reader.md +148 -0
  139. package/reference/sql/db-tx.md +114 -0
  140. package/reference/sql/db-value.md +41 -0
  141. package/reference/sql/ddl.md +85 -0
  142. package/reference/sql/frag.md +288 -0
  143. package/reference/sql/index.md +341 -0
  144. package/reference/sql/repo.md +600 -0
  145. package/reference/sql/sql-dialect.md +73 -0
  146. package/reference/sql/sql-logger.md +62 -0
  147. package/reference/sql/sql-name-mapper.md +70 -0
  148. package/reference/sql/table-metadata.md +134 -0
  149. package/reference/sql/table.md +448 -0
  150. package/reference/sql/transactor-zio.md +399 -0
  151. package/reference/sql/transactor.md +363 -0
  152. package/reference/sql-zio.md +112 -0
  153. package/reference/streams/core/index.md +32 -0
  154. package/reference/streams/core/pipeline.md +854 -0
  155. package/reference/streams/core/sink.md +1404 -0
  156. package/reference/streams/core/stream.md +3236 -0
  157. package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
  158. package/reference/streams/execution-and-compatibility/index.md +35 -0
  159. package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
  160. package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
  161. package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
  162. package/reference/streams/index.md +726 -0
  163. package/reference/streams/primitives/index.md +30 -0
  164. package/reference/streams/primitives/reader.md +1992 -0
  165. package/reference/streams/primitives/writer.md +1201 -0
  166. package/reference/telemetry/common/any-value.md +90 -0
  167. package/reference/telemetry/common/attribute-key.md +87 -0
  168. package/reference/telemetry/common/attributes.md +118 -0
  169. package/reference/telemetry/common/index.md +39 -0
  170. package/reference/telemetry/common/instrumentation-scope.md +24 -0
  171. package/reference/telemetry/common/resource.md +34 -0
  172. package/reference/telemetry/index.md +311 -0
  173. package/reference/telemetry/logging/index.md +197 -0
  174. package/reference/telemetry/logging/log-enrichment.md +72 -0
  175. package/reference/telemetry/logging/log-formatter.md +100 -0
  176. package/reference/telemetry/logging/log-record-processor.md +56 -0
  177. package/reference/telemetry/logging/log-record.md +44 -0
  178. package/reference/telemetry/logging/log-writer.md +64 -0
  179. package/reference/telemetry/logging/logger-provider.md +142 -0
  180. package/reference/telemetry/logging/logger.md +83 -0
  181. package/reference/telemetry/logging/severity.md +62 -0
  182. package/reference/telemetry/metrics/index.md +150 -0
  183. package/reference/telemetry/metrics/instruments.md +183 -0
  184. package/reference/telemetry/metrics/labeled-instruments.md +74 -0
  185. package/reference/telemetry/metrics/meter-provider.md +76 -0
  186. package/reference/telemetry/metrics/meter.md +98 -0
  187. package/reference/telemetry/metrics/metric-data.md +57 -0
  188. package/reference/telemetry/otel/custom-exporter.md +216 -0
  189. package/reference/telemetry/otel/index.md +212 -0
  190. package/reference/telemetry/tracing/index.md +155 -0
  191. package/reference/telemetry/tracing/sampler.md +89 -0
  192. package/reference/telemetry/tracing/span-builder.md +57 -0
  193. package/reference/telemetry/tracing/span-context.md +39 -0
  194. package/reference/telemetry/tracing/span-data.md +32 -0
  195. package/reference/telemetry/tracing/span-kind.md +55 -0
  196. package/reference/telemetry/tracing/span-processor.md +53 -0
  197. package/reference/telemetry/tracing/span-status.md +47 -0
  198. package/reference/telemetry/tracing/span.md +117 -0
  199. package/reference/telemetry/tracing/tracer-provider.md +91 -0
  200. package/reference/telemetry/tracing/tracer.md +52 -0
  201. package/reference/typeid.md +5 -83
  202. package/sidebars.js +376 -43
  203. package/undocumented-report.md +528 -270
  204. package/reference/formats.md +0 -694
  205. package/reference/http-model.md +0 -1716
  206. package/reference/streams.md +0 -989
  207. package/ringbuffer.md +0 -249
  208. /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
  209. /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
  210. /package/reference/{lazy.md → schema/lazy.md} +0 -0
  211. /package/reference/{reflect.md → schema/reflect.md} +0 -0
  212. /package/reference/{registers.md → schema/registers.md} +0 -0
  213. /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
  214. /package/reference/{syntax.md → schema/syntax.md} +0 -0
  215. /package/reference/{validation.md → schema/validation.md} +0 -0
@@ -0,0 +1,263 @@
1
+ ---
2
+ id: schema-search
3
+ title: "Schema Search and Update"
4
+ ---
5
+
6
+ `SchemaSearch` and `TypeSearch` are the two `DynamicOptic` node kinds that turn a path into a search: instead of naming one field, they match every value of a shape or type anywhere in a structure. This page covers the machinery behind that search once a path resolves against a value or a schema: `SchemaMatch`, the structural predicate the search matches against; `SearchTraversal`, the `Traversal[S, A]` engine that executes a search over a typed value and lets you fold, modify, or check the matches; and `Updater`, the mechanism for rewriting a `Reflect`/`Term` tree at a resolved path rather than the values it describes.
7
+
8
+ For the search DSLs themselves — the typed `.searchFor[T]` macro, the `#Pattern` string syntax, and the supported pattern grammar — see [DynamicOptic](./dynamic-optic.md#search-optics). This page assumes you can already build a search path and focuses on what runs once you have one.
9
+
10
+ ## Design & Structure
11
+
12
+ A search path splits into two phases that operate on different representations of your data:
13
+
14
+ ```
15
+ Typed value S Schema tree Reflect[F, S]
16
+ │ │
17
+ ▼ ▼
18
+ SearchTraversal[S, A] Reflect#updated(path)(Updater)
19
+ (built from .searchFor[T] (rewrites the Reflect/Term
20
+ or SearchTraversal.apply) node found at a path)
21
+ │
22
+ ▼
23
+ DynamicValue tree, walked depth-first
24
+ │
25
+ ▼
26
+ SchemaMatch.matches(pattern, value) ← only for #Pattern searches;
27
+ (the structural predicate) .searchFor[T] matches by TypeId instead
28
+ ```
29
+
30
+ `SearchTraversal` finds and transforms **values** — it decodes candidate subtrees of a concrete `S` and collects the ones that decode as `A`. `Updater` rewrites **schema metadata** — it walks the `Reflect`/`Term` tree that describes a type and replaces the node at a path, independent of any particular value. The two are easy to conflate because both start from a `DynamicOptic` path, but a `TypeSearch`/`SchemaSearch` node is only meaningful to the value-searching side: `Reflect#updated` does not special-case those nodes, so a path that reaches `Reflect#updated`/`Schema#updated` must resolve through `Field`/`Case`/`AtIndex`/`Elements`/`Wrapped`/`MapKeys`/`MapValues` nodes only.
31
+
32
+ ## SchemaMatch — Structural Pattern Matching
33
+
34
+ `SchemaMatch.matches` is the predicate a `#Pattern` search node matches against once it reaches a `DynamicValue`: given a `SchemaRepr` pattern and a `DynamicValue`, it returns whether the value has that shape:
35
+
36
+ ```scala
37
+ object SchemaMatch {
38
+ def matches(pattern: SchemaRepr, value: DynamicValue): Boolean
39
+ }
40
+ ```
41
+
42
+ `SchemaRepr` is the small pattern language `#Pattern` strings parse into — `Wildcard`, `Primitive(name)`, `Record(fields)`, `Variant(cases)`, `Sequence(element)`, `Map(key, value)`, `Optional(inner)`, and `Nominal(name)`. Constructing one directly and matching it against a `DynamicValue` shows the same rules the `#record { ... }` syntax compiles down to:
43
+
44
+ ```scala
45
+ import zio.blocks.schema._
46
+
47
+ val personPattern = SchemaRepr.Record(
48
+ IndexedSeq("name" -> SchemaRepr.Primitive("string"), "age" -> SchemaRepr.Primitive("int"))
49
+ )
50
+
51
+ val alice = DynamicValue.Record("name" -> DynamicValue.string("Alice"), "age" -> DynamicValue.int(30))
52
+ val bob = DynamicValue.Record("name" -> DynamicValue.string("Bob"), "role" -> DynamicValue.string("admin"))
53
+ ```
54
+
55
+ `Record` matching is a subset match — every field named in the pattern must exist with a matching type, but extra fields on the value are fine, which is why `bob` fails only because `age` is missing, not because of the extra `role` field:
56
+
57
+ ```scala
58
+ SchemaMatch.matches(personPattern, alice)
59
+ // res0: Boolean = true
60
+ SchemaMatch.matches(personPattern, bob)
61
+ // res1: Boolean = false
62
+ ```
63
+
64
+ `Wildcard` matches anything, and `Sequence`/`Map` patterns match when every element or entry matches — an empty sequence or map always matches, since there is nothing to fail on:
65
+
66
+ ```scala
67
+ import zio.blocks.schema._
68
+
69
+ val ages = DynamicValue.Sequence(DynamicValue.int(1), DynamicValue.int(2))
70
+ SchemaMatch.matches(SchemaRepr.Sequence(SchemaRepr.Primitive("int")), ages)
71
+ SchemaMatch.matches(SchemaRepr.Wildcard, ages)
72
+ ```
73
+
74
+ `Nominal(name)` always returns `false` against a `DynamicValue`, because a decoded value carries no record of the Scala type it came from — the same limitation documented for `#Person`-style patterns in [DynamicOptic's known limitation](./dynamic-optic.md#known-limitation-nominal-matching-in-untyped-contexts). Matching by nominal type requires the typed `.searchFor[T]` API instead, which matches by `TypeId` rather than by structure.
75
+
76
+ ## SearchTraversal — Executing a Search Over Values
77
+
78
+ `SearchTraversal` is the `Traversal[S, A]` that `.searchFor[T]` and `#Pattern` searches compile down to. Construct one directly from a pair of schemas when you want the traversal without going through the macro or the path interpolator:
79
+
80
+ ```scala
81
+ import zio.blocks.schema._
82
+
83
+ case class Address(city: String)
84
+ object Address {
85
+ implicit val schema: Schema[Address] = Schema.derived
86
+ }
87
+
88
+ case class Person(name: String, age: Int, address: Address)
89
+ object Person {
90
+ implicit val schema: Schema[Person] = Schema.derived
91
+ }
92
+
93
+ case class Team(name: String, lead: Person, members: List[Person])
94
+ object Team {
95
+ implicit val schema: Schema[Team] = Schema.derived
96
+ }
97
+
98
+ case class Company(name: String, ceo: Person, teams: List[Team])
99
+ object Company extends CompanionOptics[Company] {
100
+ implicit val schema: Schema[Company] = Schema.derived
101
+ }
102
+
103
+ val findPeople: Traversal[Company, Person] = SearchTraversal[Company, Person]
104
+
105
+ val acme = Company(
106
+ "Acme",
107
+ ceo = Person("Alice", 45, Address("NYC")),
108
+ teams = List(
109
+ Team("Platform", Person("Bob", 32, Address("SF")), List(Person("Carol", 28, Address("SF")))),
110
+ Team("Sales", Person("Dave", 39, Address("LA")), Nil)
111
+ )
112
+ )
113
+ ```
114
+
115
+ `SearchTraversal[Company, Person]` resolves the two implicit `Schema` instances the same way `Company.optic(_.searchFor[Person])` does—both produce the identical traversal, so pick whichever reads better at the call site. `Traversal#fold` walks every match depth-first, left-to-right, which is why the CEO comes before any team, and each team's lead comes before its members:
116
+
117
+ ```scala
118
+ findPeople.fold(acme)(List.empty[String], (names, p) => names :+ p.name)
119
+ // res3: List[String] = List("Alice", "Bob", "Carol", "Dave")
120
+ ```
121
+
122
+ `Traversal#modify` rewrites every match in place and rebuilds the structure around it, `modifyOption` returns `None` instead of a no-op result when nothing matched, and `modifyOrFail` surfaces a decoding failure as `Left` instead of silently keeping the original value:
123
+
124
+ ```scala
125
+ findPeople.modify(acme, p => p.copy(age = p.age + 1)).ceo.age
126
+ // res4: Int = 46
127
+ SearchTraversal[Company, Address].modifyOption(acme, a => a.copy(city = a.city.toUpperCase)).map(_.ceo.address.city)
128
+ // res5: Option[String] = Some("NYC")
129
+ SearchTraversal[Team, Address].modifyOption(acme.teams.head, a => a).isDefined
130
+ // res6: Boolean = true
131
+ ```
132
+
133
+ `Traversal#check` reports whether a traversal has at least one match, which is how a search-backed traversal signals "nothing here" without throwing:
134
+
135
+ ```scala
136
+ SearchTraversal[Company, Person].check(acme).isEmpty
137
+ // res7: Boolean = true
138
+ SearchTraversal[Team, Boolean].check(acme.teams.head).isEmpty
139
+ // res8: Boolean = false
140
+ ```
141
+
142
+ ### Composing a Search With Other Optics
143
+
144
+ A search traversal composes with `Lens`, `Prism`, `Optional`, and other `Traversal`s on either side, so you can narrow the search to part of a structure or refine each match further. Searching from a specific field finds only what is reachable from there, and appending a lens after a search projects each match down to one of its fields:
145
+
146
+ ```scala
147
+ import zio.blocks.schema._
148
+
149
+ object CompanyOptics extends CompanionOptics[Company] {
150
+ implicit val schema: Schema[Company] = Company.schema
151
+
152
+ // Search restricted to one team: only Bob and Carol, never Alice or Dave
153
+ val platformMembers: Traversal[Company, Person] = optic(_.teams.at(0).searchFor[Person])
154
+
155
+ // Search first, then focus each match's city
156
+ val allCities: Traversal[Company, String] = optic(_.searchFor[Address].city)
157
+ }
158
+ ```
159
+
160
+ Both directions rebuild the same way a plain path-based traversal does: `Traversal#modify`, `Traversal#fold`, and `Traversal#check` all work on the composed traversal exactly as they do on a bare `SearchTraversal`, because composition produces another `Traversal[S, A]` — the fact that a search sits inside it is an implementation detail, not a different API.
161
+
162
+ ### Recursive Types Are Safe to Search
163
+
164
+ Because `SearchTraversal` walks the decoded `DynamicValue` of a concrete value rather than the schema definition, searching a recursive type (a tree, a linked structure) terminates naturally — the value itself is always finite, even though its `Reflect` describes an unbounded type. There is nothing extra to opt into; a self-referential case class searches the same way a flat one does.
165
+
166
+ ## Updater — Rewriting Schema Metadata
167
+
168
+ `Reflect.Updater` and `Term.Updater` are the callbacks behind `Schema#updated` and `Reflect#updated` — the mechanism for rewriting a schema's metadata (documentation, defaults, validations, or a field's shape entirely) at a resolved path, as opposed to rewriting the values that schema describes:
169
+
170
+ ```scala
171
+ object Reflect {
172
+ trait Updater[F[_, _]] {
173
+ def update[A](reflect: Reflect[F, A]): Reflect[F, A]
174
+ }
175
+ }
176
+
177
+ object Term {
178
+ trait Updater[F[_, _]] {
179
+ def update[S, A](input: Term[F, S, A]): Option[Term[F, S, A]]
180
+ }
181
+ }
182
+ ```
183
+
184
+ The two differ in one important way: `Reflect.Updater#update` is total — it always returns a `Reflect`, because a schema node can be re-shaped but not removed. `Term.Updater#update` is partial — returning `None` deletes the field or case the updater targets, which is how `Record#modifyField` and `Variant#modifyCase` support dropping a member rather than only renaming or retyping it.
185
+
186
+ [Schema](./schema.md#updating-nested-schemas) already covers the common case, updating one field through an optic with a plain function: `Schema[Person].updated(Person.address)(_.doc("Mailing address"))`. That convenience overload builds a `Reflect.Updater` for you. Reaching for `Reflect.Updater` directly is what you need for the case that overload cannot express — a `DynamicOptic` path built at runtime, or a rewrite that needs the full node rather than just its focus:
187
+
188
+ ```scala
189
+ import zio.blocks.schema._
190
+ import zio.blocks.schema.binding.Binding
191
+
192
+ case class Config(host: String, port: Int)
193
+ object Config {
194
+ implicit val schema: Schema[Config] = Schema.derived
195
+ }
196
+
197
+ // Attach documentation to a field found by a runtime-built DynamicOptic path
198
+ val documented: Option[Schema[Config]] =
199
+ Schema[Config].updated(DynamicOptic.root.field("port"))(new Reflect.Updater[Binding] {
200
+ def update[A](reflect: Reflect[Binding, A]): Reflect[Binding, A] =
201
+ reflect.doc("The TCP port the server listens on")
202
+ })
203
+ ```
204
+
205
+ `Term.Updater` operates one level up, on the named field or case itself rather than its value, which is what makes rename and delete possible. Renaming reuses the term's existing `value`; deleting a field returns `None` and the field disappears from the record entirely:
206
+
207
+ ```scala
208
+ import zio.blocks.schema._
209
+ import zio.blocks.schema.binding.Binding
210
+
211
+ case class LegacyUser(id: Long, username: String, internalNotes: String)
212
+ object LegacyUser {
213
+ implicit val schema: Schema[LegacyUser] = Schema.derived
214
+ }
215
+
216
+ val userRecord = Schema[LegacyUser].reflect.asRecord.get
217
+
218
+ // Rename username -> name
219
+ val renamed = userRecord.modifyField("username")(new Term.Updater[Binding] {
220
+ def update[S, A](input: Term[Binding, S, A]): Option[Term[Binding, S, A]] =
221
+ Some(input.copy(name = "name"))
222
+ })
223
+
224
+ // Drop internalNotes entirely by returning None
225
+ val withoutNotes = userRecord.modifyField("internalNotes")(new Term.Updater[Binding] {
226
+ def update[S, A](input: Term[Binding, S, A]): Option[Term[Binding, S, A]] = None
227
+ })
228
+ ```
229
+
230
+ Both updaters ran against the same `userRecord` independently, so `renamed` still has three fields with one renamed, while `withoutNotes` has two:
231
+
232
+ ```scala
233
+ renamed.map(_.fields.map(_.name))
234
+ // res11: Option[IndexedSeq[String]] = Some(
235
+ // Vector("id", "name", "internalNotes")
236
+ // )
237
+ withoutNotes.map(_.fields.map(_.name))
238
+ // res12: Option[IndexedSeq[String]] = None
239
+ ```
240
+
241
+ :::warning[Search nodes are not valid `updated` paths]
242
+ `Reflect#updated`/`Schema#updated` walk a `DynamicOptic` through `Field`, `Case`, `AtIndex`/`AtIndices`/`Elements`, `Wrapped`, and `MapKeys`/`MapValues` nodes only. A path containing a `TypeSearch` or `SchemaSearch` node is not rejected outright, but it is not handled either — it falls into the same branch as `MapKeys`/`MapValues` and produces an unspecified result. Build search-based rewrites with `SearchTraversal#modify` on a value instead of `Schema#updated` on a schema. `DynamicMigration`, by contrast, rejects search nodes outright with an explicit error, since a migration requires one statically-known path.
243
+ :::
244
+
245
+ ## Where Search Nodes Are (and Aren't) Handled
246
+
247
+ The same `TypeSearch`/`SchemaSearch` node means different things depending on which API resolves the path it's part of:
248
+
249
+ | API | Search nodes |
250
+ | -------------------------- | ---------------------------------------------------------- |
251
+ | `SearchTraversal` / `.searchFor[T]` / `#Pattern` on a value | The intended use — this is what builds and executes the search |
252
+ | `Reflect#get` / `Schema#get` | Supported — resolves the first match, then continues the remaining path from there |
253
+ | `Json` / `DynamicValue` patch paths (`JsonPatch`) | Supported — rewrites every match, not just the first |
254
+ | `Reflect#updated` / `Schema#updated` | Not handled — falls through to the map-key/value branch; use `SearchTraversal#modify` instead |
255
+ | `DynamicMigration` | Rejected outright with `"Type/Schema search nodes are not supported in migration paths"` |
256
+
257
+ ## See Also
258
+
259
+ - [DynamicOptic](./dynamic-optic.md#search-optics) — the `.searchFor[T]` and `#Pattern` search DSLs, and the full pattern grammar table.
260
+ - [Path Interpolator](./path-interpolator.md) — the `p"..."` string syntax that `#Pattern` search nodes parse from.
261
+ - [Schema](./schema.md#updating-nested-schemas) — `Schema#updated` and `Schema#@@` for the common, optic-based case of rewriting one field's metadata.
262
+ - [Reflect](./reflect.md) — the node types (`Record`, `Variant`, `Sequence`, `Map`, `Wrapper`, `Deferred`) that `Updater` rewrites and `SearchTraversal` decodes against.
263
+ - [Optics](./optics.md) — the base `Traversal[S, A]` type that `SearchTraversal` implements and composes with.
@@ -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):
@@ -510,6 +522,8 @@ val updated: Option[Schema[Person]] = Schema[Person]
510
522
  .updated(Person.address)(_.doc("Mailing address"))
511
523
  ```
512
524
 
525
+ For the lower-level `Reflect.Updater`/`Term.Updater` callbacks this method builds on — including how `Term.Updater` can rename or delete a field by returning `None` — see [Schema Search and Update](./schema-search.md#updater--rewriting-schema-metadata).
526
+
513
527
  ## Schema Aspects
514
528
 
515
529
  Schema aspects are a powerful mechanism in ZIO Blocks for transforming schemas. You can think of the schema aspect as a function that takes a reflect and produces a new reflect:
@@ -517,17 +531,21 @@ Schema aspects are a powerful mechanism in ZIO Blocks for transforming schemas.
517
531
  ```scala
518
532
  trait SchemaAspect[-Upper, +Lower, F[_, _]] {
519
533
  def apply[A >: Lower <: Upper](reflect: Reflect[F, A]): Reflect[F, A]
520
- def recursive(implicit ev1: Any <:< Upper, ev2: Lower <:< Nothing): SchemaAspect[Upper, Lower, F]
521
534
  }
522
535
  ```
523
536
 
524
- The `Schema` data type has a `@@` method used for applying schema aspects:
537
+ The `Schema` data type has a `@@` method that applies schema aspects, and it's a thin wrapper over two `Reflect#aspect` overloads: applying an aspect to a whole schema just calls `aspect(reflect)` directly, and applying one at a path uses [`Reflect#updated`](./reflect-transformer.md) to rewrite the subtree the optic points to:
525
538
 
526
539
  ```scala
527
540
  case class Schema[A](reflect: Reflect.Bound[A]) {
528
541
  def @@[Min >: A, Max <: A](aspect: SchemaAspect[Min, Max, Binding]): Schema[A] = ???
529
542
  def @@[B](part: Optic[A, B], aspect: SchemaAspect[B, B, Binding]) = ???
530
543
  }
544
+
545
+ sealed trait Reflect[F[_, _], A] {
546
+ def aspect[Min >: A, Max <: A](aspect: SchemaAspect[Min, Max, F]): Reflect[F, A]
547
+ def aspect[B, Min >: B, Max <: B](optic: Optic[A, B], aspect: SchemaAspect[Min, Max, F]): Reflect[F, A]
548
+ }
531
549
  ```
532
550
 
533
551
  These methods enable us to use `@@` syntax for applying aspects to either the entire schema or a specific path within the schema using optics:
@@ -551,6 +569,8 @@ Currently, ZIO Blocks provides the following built-in schema aspects:
551
569
  - `SchemaAspect.doc`: Attach documentation to schema or field
552
570
  - `SchemaAspect.examples`: Attach example values to schema or field
553
571
 
572
+ The path-targeted overload doesn't fail on an optic that doesn't resolve against the schema — it falls back to the original schema unchanged, the same behavior [`Reflect#updated`](./reflect-transformer.md) has when a path finds nothing. This matters most when an optic is built by hand rather than through the `optic(_.field)` macro: a lens pointing at a field name that isn't actually on the record silently leaves the schema untouched rather than throwing.
573
+
554
574
  ## Modifiers
555
575
 
556
576
  Modifiers in ZIO Blocks provide a mechanism to attach metadata and configuration to schema elements without polluting the domain types themselves. They serve as the successor to ZIO Schema 1's annotation system, with the critical advantage of being **pure data** so, unlike Scala annotations, modifiers are runtime values that can be serialized.
@@ -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
@@ -167,6 +167,68 @@ import zio.blocks.schema.json._
167
167
  val jsonCodec = Person.schema.derive(JsonFormat)
168
168
  ```
169
169
 
170
+ ## Customizing Derivation with Instance and Modifier Overrides
171
+
172
+ By default, `Deriver` automatically derives codecs for all types. But sometimes you need to customize how specific types are encoded or decoded—for example, encoding `LocalDate` as `"dd/MM/yyyy"` instead of ISO format.
173
+
174
+ ZIO Blocks provides three levels of customization, in progressive order:
175
+
176
+ 1. **Type-level override** (`Deriver.withInstance`) — Override the codec for ALL occurrences of a type. Configure once, use everywhere:
177
+
178
+ ```scala
179
+ import java.time.LocalDate
180
+ import java.time.format.DateTimeFormatter
181
+
182
+ // Create a custom JsonCodec for LocalDate
183
+ val customDateCodec: JsonCodec[LocalDate] = new JsonCodec[LocalDate] {
184
+ private val fmt = DateTimeFormatter.ofPattern("dd/MM/yyyy")
185
+ def decodeValue(in: JsonReader): LocalDate = LocalDate.parse(in.readString(), fmt)
186
+ def encodeValue(x: LocalDate, out: JsonWriter): Unit = out.writeVal(fmt.format(x))
187
+ // ... AST overrides ...
188
+ }
189
+
190
+ // Configure the deriver once
191
+ val myDeriver = JsonCodecDeriver.withInstance[LocalDate](customDateCodec)
192
+
193
+ // Use everywhere — all LocalDate fields use the custom codec
194
+ val codec1 = Schema[Event].deriving(myDeriver).derive
195
+ val codec2 = Schema[Meeting].deriving(myDeriver).derive
196
+ ```
197
+
198
+ 2. **Field-level override** (`Deriver.withInstance` with typeId + termName) — Override a specific field only:
199
+
200
+ ```scala
201
+ // Only Event.date uses custom format; other LocalDate fields are unchanged
202
+ val deriver = JsonCodecDeriver.withInstance[Event, LocalDate](
203
+ TypeId.of[Event], "date", customDateCodec
204
+ )
205
+ ```
206
+
207
+ 3. **Modifier override** (`Deriver.withModifier`) — Rename fields, add aliases:
208
+
209
+ ```scala
210
+ val deriver = JsonCodecDeriver.withModifier(
211
+ TypeId.of[Person], "firstName", Modifier.rename("first_name")
212
+ )
213
+ ```
214
+
215
+ ### Chaining Overrides
216
+
217
+ Overrides compose, allowing you to build complex derivers incrementally:
218
+
219
+ ```scala
220
+ val myDeriver = JsonCodecDeriver
221
+ .withInstance[LocalDate](customDateCodec)
222
+ .withModifier(TypeId.of[Event], "name", Modifier.rename("title"))
223
+ ```
224
+
225
+ ### Important Notes
226
+
227
+ - `withInstance` and `withModifier` return a NEW deriver (they are immutable).
228
+ - When combined with `DerivationBuilder`, deriver-level instance overrides take precedence over builder-level instance overrides. Modifier override precedence is order-sensitive and should not be assumed to follow the same rule.
229
+ - Unknown `termName` values are silently ignored.
230
+ - The `B` type parameter in field-level `withInstance` is not statically checked against the actual field type.
231
+
170
232
  ## Example 1: Deriving a `Show` Type Class Instance
171
233
 
172
234
  Let's say we want to derive a `Show` type class instance for any type of type `A`:
@@ -1455,7 +1517,7 @@ Now we can use the derived `Gen[Person]` instance to generate random `Person` va
1455
1517
 
1456
1518
  ```scala
1457
1519
  val random = new Random(42) // Seeded for reproducible output
1458
- // random: Random = scala.util.Random@643bb7d2
1520
+ // random: Random = scala.util.Random@57cbd935
1459
1521
 
1460
1522
  Person.gen.generate(random)
1461
1523
  // res14: Person = Person(name = "p", age = -1360544799)