@zio.dev/zio-blocks 0.0.51 → 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 (164) 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 +6 -0
  4. package/guides/getting-started-with-mux.md +0 -112
  5. package/guides/query-dsl-extending.md +1 -1
  6. package/guides/query-dsl-fluent-builder.md +1 -1
  7. package/guides/query-dsl-reified-optics.md +1 -1
  8. package/guides/query-dsl-sql.md +395 -1
  9. package/guides/sql-checked-interpolation.md +173 -0
  10. package/guides/sql-transactions.md +286 -0
  11. package/guides/telemetry-guide.md +131 -70
  12. package/guides/zio-schema-migration.md +6 -6
  13. package/index.md +200 -583
  14. package/package.json +1 -1
  15. package/reference/async.md +1379 -531
  16. package/reference/chunk.md +3 -3
  17. package/reference/codegen/index.md +1 -1
  18. package/reference/combinators.md +4 -4
  19. package/reference/config/config-decoder.md +460 -0
  20. package/reference/config/config-source.md +489 -0
  21. package/reference/config/errors.md +278 -0
  22. package/reference/config/flags.md +369 -0
  23. package/reference/config/formats.md +314 -0
  24. package/reference/config/index.md +304 -0
  25. package/reference/config/rollout.md +336 -0
  26. package/reference/context.md +6 -49
  27. package/reference/data-migration.md +269 -0
  28. package/reference/datastar/attributes.md +302 -0
  29. package/reference/datastar/events.md +234 -0
  30. package/reference/datastar/index.md +256 -0
  31. package/reference/datastar/signals.md +230 -0
  32. package/reference/datastar/sse.md +295 -0
  33. package/reference/datastar.md +2 -2
  34. package/reference/docs.md +2 -2
  35. package/reference/endpoint/bulk-creation.md +96 -0
  36. package/reference/endpoint/index.md +9 -89
  37. package/reference/endpoint/path-codec.md +12 -24
  38. package/reference/endpoint/route-pattern.md +4 -6
  39. package/reference/endpoint/segment-codec.md +19 -32
  40. package/reference/html.md +313 -9
  41. package/reference/htmx/index.md +4 -52
  42. package/reference/htmx/response-headers.md +240 -0
  43. package/reference/http-model/headers.md +735 -0
  44. package/reference/http-model/index.md +3 -1
  45. package/reference/http-model/model.md +107 -71
  46. package/reference/http-model/schema-codecs.md +522 -0
  47. package/reference/http-model/schema.md +6 -3
  48. package/reference/http-model/server-sent-event.md +341 -0
  49. package/reference/jwt.md +195 -0
  50. package/reference/maybe.md +128 -11
  51. package/reference/media-type.md +2 -2
  52. package/reference/mux.md +254 -0
  53. package/reference/mux.mdx +7 -2
  54. package/reference/openapi.md +3 -3
  55. package/reference/projection.md +654 -0
  56. package/reference/resource-management/resource.md +2 -98
  57. package/reference/resource-management/scope.md +1 -209
  58. package/reference/resource-management/wire.md +4 -50
  59. package/reference/ringbuffer/advanced.mdx +1 -1
  60. package/reference/ringbuffer/index.mdx +3 -3
  61. package/reference/ringbuffer/mpmc.mdx +38 -4
  62. package/reference/ringbuffer/mpsc.mdx +36 -4
  63. package/reference/ringbuffer/spmc.mdx +1 -1
  64. package/reference/ringbuffer/spsc.mdx +87 -15
  65. package/reference/schema/allows.md +0 -96
  66. package/reference/schema/binding.md +2 -2
  67. package/reference/schema/built-in-codecs/avro.md +2 -2
  68. package/reference/schema/built-in-codecs/bson.md +50 -20
  69. package/reference/schema/built-in-codecs/csv.md +2 -2
  70. package/reference/schema/built-in-codecs/index.md +3 -3
  71. package/reference/schema/built-in-codecs/json/index.md +2 -2
  72. package/reference/schema/built-in-codecs/messagepack.md +3 -3
  73. package/reference/schema/built-in-codecs/thrift.md +2 -2
  74. package/reference/schema/built-in-codecs/toon.md +3 -3
  75. package/reference/schema/built-in-codecs/yaml.md +2 -2
  76. package/reference/schema/codec.md +11 -11
  77. package/reference/schema/dynamic-optic.md +48 -3
  78. package/reference/schema/dynamic-schema.md +3 -3
  79. package/reference/schema/index.md +2 -0
  80. package/reference/schema/path-interpolator.md +2 -0
  81. package/reference/schema/reflect-transformer.md +140 -0
  82. package/reference/schema/schema-evolution/as.md +4 -4
  83. package/reference/schema/schema-evolution/into.md +2 -2
  84. package/reference/schema/schema-expr.md +2 -2
  85. package/reference/schema/schema-search.md +263 -0
  86. package/reference/schema/schema.md +10 -2
  87. package/reference/schema/type-class-derivation.md +1 -1
  88. package/reference/smithy.md +502 -3
  89. package/reference/sql/db-codec-deriver.md +3 -3
  90. package/reference/sql/db-codec.md +22 -22
  91. package/reference/sql/db-con.md +4 -4
  92. package/reference/sql/db-connection.md +1 -1
  93. package/reference/sql/db-param.md +1 -1
  94. package/reference/sql/db-result-reader.md +4 -2
  95. package/reference/sql/db-tx.md +46 -14
  96. package/reference/sql/ddl.md +1 -1
  97. package/reference/sql/frag.md +44 -10
  98. package/reference/sql/index.md +7 -7
  99. package/reference/sql/repo.md +15 -15
  100. package/reference/sql/sql-dialect.md +1 -1
  101. package/reference/sql/sql-logger.md +1 -1
  102. package/reference/sql/sql-name-mapper.md +3 -3
  103. package/reference/sql/table-metadata.md +3 -3
  104. package/reference/sql/table.md +10 -10
  105. package/reference/sql/transactor-zio.md +1 -1
  106. package/reference/sql/transactor.md +21 -11
  107. package/reference/sql-zio.md +1 -1
  108. package/reference/streams/core/index.md +32 -0
  109. package/reference/streams/{pipeline.md → core/pipeline.md} +210 -74
  110. package/reference/streams/{sink.md → core/sink.md} +331 -353
  111. package/reference/streams/{stream.md → core/stream.md} +919 -209
  112. package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
  113. package/reference/streams/execution-and-compatibility/index.md +35 -0
  114. package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
  115. package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
  116. package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
  117. package/reference/streams/index.md +140 -67
  118. package/reference/streams/primitives/index.md +30 -0
  119. package/reference/streams/primitives/reader.md +1992 -0
  120. package/reference/streams/{writer.md → primitives/writer.md} +254 -98
  121. package/reference/telemetry/common/any-value.md +90 -0
  122. package/reference/telemetry/common/attribute-key.md +87 -0
  123. package/reference/telemetry/common/attributes.md +118 -0
  124. package/reference/telemetry/common/index.md +39 -0
  125. package/reference/telemetry/common/instrumentation-scope.md +24 -0
  126. package/reference/telemetry/common/resource.md +34 -0
  127. package/reference/telemetry/index.md +311 -0
  128. package/reference/telemetry/logging/index.md +197 -0
  129. package/reference/telemetry/logging/log-enrichment.md +72 -0
  130. package/reference/telemetry/logging/log-formatter.md +100 -0
  131. package/reference/telemetry/logging/log-record-processor.md +56 -0
  132. package/reference/telemetry/logging/log-record.md +44 -0
  133. package/reference/telemetry/logging/log-writer.md +64 -0
  134. package/reference/telemetry/logging/logger-provider.md +142 -0
  135. package/reference/telemetry/logging/logger.md +83 -0
  136. package/reference/telemetry/logging/severity.md +62 -0
  137. package/reference/telemetry/metrics/index.md +150 -0
  138. package/reference/telemetry/metrics/instruments.md +183 -0
  139. package/reference/telemetry/metrics/labeled-instruments.md +74 -0
  140. package/reference/telemetry/metrics/meter-provider.md +76 -0
  141. package/reference/telemetry/metrics/meter.md +98 -0
  142. package/reference/telemetry/metrics/metric-data.md +57 -0
  143. package/reference/telemetry/otel/custom-exporter.md +216 -0
  144. package/reference/telemetry/otel/index.md +212 -0
  145. package/reference/telemetry/tracing/index.md +155 -0
  146. package/reference/telemetry/tracing/sampler.md +89 -0
  147. package/reference/telemetry/tracing/span-builder.md +57 -0
  148. package/reference/telemetry/tracing/span-context.md +39 -0
  149. package/reference/telemetry/tracing/span-data.md +32 -0
  150. package/reference/telemetry/tracing/span-kind.md +55 -0
  151. package/reference/telemetry/tracing/span-processor.md +53 -0
  152. package/reference/telemetry/tracing/span-status.md +47 -0
  153. package/reference/telemetry/tracing/span.md +117 -0
  154. package/reference/telemetry/tracing/tracer-provider.md +91 -0
  155. package/reference/telemetry/tracing/tracer.md +52 -0
  156. package/reference/typeid.md +0 -64
  157. package/sidebars.js +150 -12
  158. package/undocumented-report.md +528 -270
  159. package/reference/config.md +0 -158
  160. package/reference/streams/concurrent-operators.md +0 -106
  161. package/reference/streams/reader.md +0 -1284
  162. package/reference/streams/scala-2-compatibility.md +0 -55
  163. package/reference/streams/zero-boxing.md +0 -275
  164. package/reference/telemetry.md +0 -693
@@ -86,13 +86,13 @@ The practical result is that Chunk gives you a sequence type that's genuinely ef
86
86
  Chunk is available in the core `zio-blocks` library:
87
87
 
88
88
  ```scala
89
- libraryDependencies += "dev.zio" %% "zio-blocks-chunk" % "0.0.51"
89
+ libraryDependencies += "dev.zio" %% "zio-blocks-chunk" % "0.0.55"
90
90
  ```
91
91
 
92
92
  For Scala.js support:
93
93
 
94
94
  ```scala
95
- libraryDependencies += "dev.zio" %%% "zio-blocks-chunk" % "0.0.51"
95
+ libraryDependencies += "dev.zio" %%% "zio-blocks-chunk" % "0.0.55"
96
96
  ```
97
97
 
98
98
  Supports Scala 2.13.x and 3.x.
@@ -489,7 +489,7 @@ import zio.blocks.chunk.Chunk
489
489
  import java.util.Arrays
490
490
 
491
491
  val javaIter = Arrays.asList(1, 2, 3).iterator()
492
- // javaIter: Iterator[Int] = java.util.Arrays$ArrayItr@29c50fed
492
+ // javaIter: Iterator[Int] = java.util.Arrays$ArrayItr@1633b83e
493
493
  val chunk = Chunk.fromJavaIterator(javaIter)
494
494
  // chunk: Chunk[Int] = IndexedSeq(1, 2, 3)
495
495
  ```
@@ -81,7 +81,7 @@ OpenAPI → Scala Smithy → Scala Protobuf → Scala JSON Schema → S
81
81
  Add the library to your project:
82
82
 
83
83
  ```scala
84
- libraryDependencies += "dev.zio" %% "zio-blocks-codegen" % "0.0.51"
84
+ libraryDependencies += "dev.zio" %% "zio-blocks-codegen" % "0.0.55"
85
85
  ```
86
86
 
87
87
  Supported Scala versions: 2.13.x and 3.x
@@ -29,13 +29,13 @@ All typeclasses are derived automatically via compile-time resolution and provid
29
29
  Add the following to your `build.sbt`:
30
30
 
31
31
  ```sbt
32
- libraryDependencies += "dev.zio" %% "zio-blocks-combinators" % "0.0.51"
32
+ libraryDependencies += "dev.zio" %% "zio-blocks-combinators" % "0.0.55"
33
33
  ```
34
34
 
35
35
  For cross-platform projects (Scala.js):
36
36
 
37
37
  ```sbt
38
- libraryDependencies += "dev.zio" %%% "zio-blocks-combinators" % "0.0.51"
38
+ libraryDependencies += "dev.zio" %%% "zio-blocks-combinators" % "0.0.55"
39
39
  ```
40
40
 
41
41
  Supported platforms:
@@ -286,7 +286,7 @@ import zio.blocks.combinators.Eithers
286
286
  val e = summon[Eithers.Eithers[Int, String]]
287
287
  // e: Eithers[Int, String] {
288
288
  type Out >: Either[Int, String] <: Either[Int, String]
289
- } = zio.blocks.combinators.Eithers$Eithers$AtomicInstance@65d7b306
289
+ } = zio.blocks.combinators.Eithers$Eithers$AtomicInstance@533b9d04
290
290
  val input = Left(42): Either[Int, String]
291
291
  // input: Either[Int, String] = Left(42)
292
292
  e.separate(e.combine(input))
@@ -379,7 +379,7 @@ import zio.blocks.combinators.Unions
379
379
  val u = summon[Unions.Unions.WithOut[Int, String, Int | String]]
380
380
  // u: Unions[Int, String] {
381
381
  type Out >: Int | String <: Int | String
382
- } = zio.blocks.combinators.Unions$Unions$UnionInstance@64a090cd
382
+ } = zio.blocks.combinators.Unions$Unions$UnionInstance@17919b2f
383
383
  u.separate(42: Int | String)
384
384
  // res14: Either[Int, String] = Left(42)
385
385
  u.separate("hello": Int | String)
@@ -0,0 +1,460 @@
1
+ ---
2
+ id: config-decoder
3
+ title: "ConfigDecoder"
4
+ sidebar_label: "ConfigDecoder"
5
+ ---
6
+
7
+ `ConfigDecoder[A]` reads a value of type `A` out of a `ConfigSource`, accumulating every failure instead of stopping at the first one. Instances are derived from `Schema[A]` by `ConfigDecoderDeriver`, which defines how records, variants, sequences, maps, and primitives map onto dot-separated keys. The trait and its derivation entry point:
8
+
9
+ ```scala
10
+ trait ConfigDecoder[A] {
11
+ def decode(source: ConfigSource, prefix: String): Either[::[ConfigError], A]
12
+ }
13
+
14
+ object ConfigDecoder {
15
+ def apply[A](implicit decoder: ConfigDecoder[A]): ConfigDecoder[A]
16
+ def derive[A](implicit schema: Schema[A]): ConfigDecoder[A]
17
+ }
18
+ ```
19
+
20
+ ## Motivation
21
+
22
+ Writing a config decoder by hand means writing the same code once per field: look up a key, parse the string, handle the missing case, collect the error. For a twenty-field config that is twenty near-identical blocks that drift out of sync with the case class the moment someone adds a field.
23
+
24
+ `Schema[A]` already knows the field names, the types, and the defaults. `ConfigDecoderDeriver` turns that knowledge into the lookup-and-parse loop, so adding a field to a case class adds a key to the config with no other change. The derivation is a runtime walk over the schema, which means it is also introspectable: the same `Deriver` machinery lets you override a single type's decoding without rewriting the rest.
25
+
26
+ The `prefix` parameter exists so decoders compose. A record decoder calls its field decoders with `s"$prefix.$fieldName"`, and the recursion bottoms out at primitives that look up exactly one key.
27
+
28
+ ## Deriving a Decoder
29
+
30
+ `ConfigDecoder.derive` builds a decoder from an implicit `Schema[A]` using the default deriver:
31
+
32
+ ```scala
33
+ import zio.blocks.config._
34
+ import zio.blocks.schema.Schema
35
+
36
+ case class Db(host: String, port: Int)
37
+
38
+ object Db {
39
+ implicit val schema: Schema[Db] = Schema.derived[Db]
40
+ }
41
+
42
+ val decoder = ConfigDecoder.derive[Db]
43
+ ```
44
+
45
+ Decoding with an empty prefix treats field names as top-level keys:
46
+
47
+ ```scala
48
+ decoder.decode(ConfigSource.fromMap(Map("host" -> "localhost", "port" -> "5432")), "")
49
+ // res0: Either[::[ConfigError], Db] = Right(
50
+ // Db(host = "localhost", port = 5432)
51
+ // )
52
+ ```
53
+
54
+ Passing a non-empty prefix roots the same decoder under a subtree, which is how one decoder serves both a standalone config file and a section of a larger one:
55
+
56
+ ```scala
57
+ decoder.decode(ConfigSource.fromMap(Map("db.host" -> "localhost", "db.port" -> "5432")), "db")
58
+ // res1: Either[::[ConfigError], Db] = Right(
59
+ // Db(host = "localhost", port = 5432)
60
+ // )
61
+ ```
62
+
63
+ `Config.load[A]` is a thin wrapper over these two lines. The difference matters for repeated loads: `Config.load` derives a fresh decoder on every call, while a decoder you hold onto is derived once.
64
+
65
+ :::tip[Derive once for repeated loads]
66
+ Derivation walks the entire schema and allocates a decoder per field. For a config reloaded on a timer, or decoded per request, call `ConfigDecoder.derive[A]` at startup and reuse the instance.
67
+ :::
68
+
69
+ ## Mapping Rules
70
+
71
+ The deriver has one rule per schema shape. Together they determine every key your config file needs, so this section is the reference for "what key does field X read?".
72
+
73
+ ### Records
74
+
75
+ A record's fields become keys under the record's prefix, joined with dots. Nesting composes: a field whose type is itself a record contributes another path segment:
76
+
77
+ ```scala
78
+ import zio.blocks.config._
79
+ import zio.blocks.schema.Schema
80
+
81
+ case class Db(host: String, port: Int)
82
+
83
+ object Db {
84
+ implicit val schema: Schema[Db] = Schema.derived[Db]
85
+ }
86
+
87
+ case class Http(host: String, port: Int)
88
+
89
+ object Http {
90
+ implicit val schema: Schema[Http] = Schema.derived[Http]
91
+ }
92
+
93
+ case class App(db: Db, http: Http)
94
+
95
+ object App {
96
+ implicit val schema: Schema[App] = Schema.derived[App]
97
+ }
98
+ ```
99
+
100
+ Four keys, two per nested record, all reachable from one top-level decode:
101
+
102
+ ```scala
103
+ Config.load[App](
104
+ ConfigSource.fromMap(
105
+ Map(
106
+ "db.host" -> "dbhost",
107
+ "db.port" -> "5432",
108
+ "http.host" -> "0.0.0.0",
109
+ "http.port" -> "8080"
110
+ )
111
+ )
112
+ )
113
+ // res3: Either[::[ConfigError], App] = Right(
114
+ // App(
115
+ // db = Db(host = "dbhost", port = 5432),
116
+ // http = Http(host = "0.0.0.0", port = 8080)
117
+ // )
118
+ // )
119
+ ```
120
+
121
+ The deriver uses field names verbatim, in the casing the Scala source declares. To read a differently-cased namespace, wrap the source with `ConfigSource#keyFormat` rather than renaming fields — see [ConfigSource](./config-source.md).
122
+
123
+ ### Primitives
124
+
125
+ A primitive field looks up exactly one key and parses the string. Parsing rules by type:
126
+
127
+ | Schema type | Accepted form | Example |
128
+ | -------------------------------------------------------- | ---------------------------------------------- | -------------- |
129
+ | `Boolean` | `true`/`false`, `1`/`0`, `yes`/`no`, `on`/`off` (any case) | `on` |
130
+ | `Byte`, `Short`, `Int`, `Long` | Decimal integer literal | `5432` |
131
+ | `Float`, `Double` | Decimal or scientific literal | `0.75` |
132
+ | `Char` | Exactly one character | `x` |
133
+ | `String` | Any string, taken as-is | `localhost` |
134
+ | `BigInt`, `BigDecimal` | Arbitrary-precision numeric literal | `1e40` |
135
+ | `UUID` | Canonical 36-character form | `f81d4fae-…` |
136
+ | `java.time.Duration` | ISO-8601 duration | `PT30S` |
137
+ | `Instant`, `LocalDate`, `LocalDateTime`, `LocalTime` | ISO-8601 | `2026-08-21` |
138
+ | `OffsetDateTime`, `OffsetTime`, `ZonedDateTime` | ISO-8601 with offset or zone | `2026-08-21T00:00:00Z` |
139
+ | `ZoneId`, `ZoneOffset` | Zone name or offset | `Europe/Berlin` |
140
+ | `Period`, `Year`, `YearMonth`, `MonthDay` | ISO-8601 | `P1M` |
141
+ | `Month`, `DayOfWeek` | Enum name, uppercased before lookup | `monday` |
142
+ | `Currency` | ISO 4217 code | `EUR` |
143
+ | `Unit` | Any value; the key must exist | `()` |
144
+
145
+ A value that fails to parse produces `ConfigError.InvalidValue` naming the path, the offending string, the expected type, and the source:
146
+
147
+ ```scala
148
+ import zio.blocks.config._
149
+ import zio.blocks.schema.Schema
150
+
151
+ case class Db(host: String, port: Int)
152
+
153
+ object Db {
154
+ implicit val schema: Schema[Db] = Schema.derived[Db]
155
+ }
156
+ ```
157
+
158
+ The underlying parse exception is retained as the error's `cause`:
159
+
160
+ ```scala
161
+ Config.load[Db](ConfigSource.fromMap(Map("host" -> "localhost", "port" -> "not-a-number"), "bad"))
162
+ // res5: Either[::[ConfigError], Db] = Left(
163
+ // List(
164
+ // InvalidValue(
165
+ // path = "port",
166
+ // value = "not-a-number",
167
+ // expectedType = "Int",
168
+ // source = "bad",
169
+ // cause = Some(
170
+ // java.lang.NumberFormatException: For input string: "not-a-number"
171
+ // )
172
+ // )
173
+ // )
174
+ // )
175
+ ```
176
+
177
+ :::warning[Two different duration formats]
178
+ Config decoding parses `java.time.Duration` with `Duration.parse`, so it requires ISO-8601 (`PT30S`). Flag readers parse `scala.concurrent.duration.FiniteDuration` with a suffix grammar instead (`30s`). The same string is not valid in both places — see [Flags](./flags.md).
179
+ :::
180
+
181
+ ### Optional Fields
182
+
183
+ A field of type `Option[A]` decodes the inner `A` at the same key the field would otherwise use — there is no extra path segment for the `Option` itself. A missing key yields `None`:
184
+
185
+ ```scala
186
+ import zio.blocks.config._
187
+ import zio.blocks.schema.Schema
188
+
189
+ case class Service(name: String, tag: Option[String])
190
+
191
+ object Service {
192
+ implicit val schema: Schema[Service] = Schema.derived[Service]
193
+ }
194
+ ```
195
+
196
+ Supplying the key produces `Some`, and omitting it produces `None`:
197
+
198
+ ```scala
199
+ Config.load[Service](ConfigSource.fromMap(Map("name" -> "api", "tag" -> "v2")))
200
+ // res7: Either[::[ConfigError], Service] = Right(
201
+ // Service(name = "api", tag = Some("v2"))
202
+ // )
203
+ Config.load[Service](ConfigSource.fromMap(Map("name" -> "api")))
204
+ // res8: Either[::[ConfigError], Service] = Right(
205
+ // Service(name = "api", tag = None)
206
+ // )
207
+ ```
208
+
209
+ The distinction that matters is *missing* versus *invalid*. An optional field absorbs missing keys only. A key that is present but unparseable still fails the whole decode, because silently returning `None` for a typo would hide the mistake.
210
+
211
+ ### Default Values
212
+
213
+ A field with a default in the case class falls back to that default when its key is missing, exactly as an `Option` falls back to `None`:
214
+
215
+ ```scala
216
+ import zio.blocks.config._
217
+ import zio.blocks.schema.Schema
218
+
219
+ case class Db(host: String, port: Int = 5432)
220
+
221
+ object Db {
222
+ implicit val schema: Schema[Db] = Schema.derived[Db]
223
+ }
224
+ ```
225
+
226
+ Omitting `port` uses the declared default rather than reporting a missing key:
227
+
228
+ ```scala
229
+ Config.load[Db](ConfigSource.fromMap(Map("host" -> "localhost")))
230
+ // res10: Either[::[ConfigError], Db] = Right(
231
+ // Db(host = "localhost", port = 5432)
232
+ // )
233
+ ```
234
+
235
+ As with optional fields, the fallback applies only to missing keys. A present-but-invalid value is an error, not a reason to use the default.
236
+
237
+ ### Sequences
238
+
239
+ A sequence is read in one of two shapes. The indexed shape uses one key per element, numbered from zero, and the deriver probes upward until it finds a gap:
240
+
241
+ ```scala
242
+ import zio.blocks.config._
243
+ import zio.blocks.schema.Schema
244
+
245
+ case class Hosts(items: List[String])
246
+
247
+ object Hosts {
248
+ implicit val schema: Schema[Hosts] = Schema.derived[Hosts]
249
+ }
250
+ ```
251
+
252
+ Indexed keys are what the YAML and JSON adapters produce when they flatten an array:
253
+
254
+ ```scala
255
+ Config.load[Hosts](
256
+ ConfigSource.fromMap(Map("items.0" -> "alpha", "items.1" -> "beta", "items.2" -> "gamma"))
257
+ )
258
+ // res12: Either[::[ConfigError], Hosts] = Right(
259
+ // Hosts(List("alpha", "beta", "gamma"))
260
+ // )
261
+ ```
262
+
263
+ When no indexed key exists, the deriver falls back to reading the prefix itself as a single comma-separated string, which is what an environment variable can realistically hold:
264
+
265
+ ```scala
266
+ Config.load[Hosts](ConfigSource.fromMap(Map("items" -> "alpha, beta, gamma")))
267
+ // res13: Either[::[ConfigError], Hosts] = Right(
268
+ // Hosts(List("alpha", "beta", "gamma"))
269
+ // )
270
+ ```
271
+
272
+ Elements are trimmed after splitting, so `"alpha, beta"` and `"alpha,beta"` are equivalent. An element containing a comma cannot be expressed in the flat form; use indexed keys for those.
273
+
274
+ Probing stops at the first index that has neither a value nor any nested keys beneath it, so `items.0` and `items.2` without `items.1` yields a one-element list rather than an error.
275
+
276
+ ### Maps
277
+
278
+ A map's keys are discovered by enumeration: the deriver calls `ConfigSource#all` on the prefix and takes the first path segment after it as a map key. Values decode at `prefix.key`:
279
+
280
+ ```scala
281
+ import zio.blocks.config._
282
+ import zio.blocks.schema.Schema
283
+
284
+ case class Limits(counts: Map[String, Int])
285
+
286
+ object Limits {
287
+ implicit val schema: Schema[Limits] = Schema.derived[Limits]
288
+ }
289
+ ```
290
+
291
+ Each distinct segment under `counts` becomes one entry:
292
+
293
+ ```scala
294
+ Config.load[Limits](ConfigSource.fromMap(Map("counts.read" -> "100", "counts.write" -> "20")))
295
+ // res15: Either[::[ConfigError], Limits] = Right(
296
+ // Limits(Map("read" -> 100, "write" -> 20))
297
+ // )
298
+ ```
299
+
300
+ Because discovery goes through `ConfigSource#all`, a map cannot be decoded from a source that does not enumerate. Map keys themselves are decoded through the key type's decoder, so a `Map[Int, String]` requires segments that parse as integers.
301
+
302
+ ### Sealed Traits
303
+
304
+ A sealed trait is decoded by reading a discriminator key that names which case to use. The default key is `type`, read at `prefix.type`:
305
+
306
+ ```scala
307
+ import zio.blocks.config._
308
+ import zio.blocks.schema.Schema
309
+
310
+ sealed trait Backend
311
+
312
+ object Backend {
313
+ case class Postgres(host: String, port: Int) extends Backend
314
+ case class Sqlite(path: String) extends Backend
315
+
316
+ implicit val schema: Schema[Backend] = Schema.derived[Backend]
317
+ }
318
+
319
+ case class Store(backend: Backend)
320
+
321
+ object Store {
322
+ implicit val schema: Schema[Store] = Schema.derived[Store]
323
+ }
324
+ ```
325
+
326
+ The discriminator selects the case, and the remaining keys are read at the same prefix as the case's own fields:
327
+
328
+ ```scala
329
+ Config.load[Store](
330
+ ConfigSource.fromMap(
331
+ Map("backend.type" -> "Postgres", "backend.host" -> "localhost", "backend.port" -> "5432")
332
+ )
333
+ )
334
+ // res17: Either[::[ConfigError], Store] = Right(
335
+ // Store(Postgres(host = "localhost", port = 5432))
336
+ // )
337
+ ```
338
+
339
+ A missing discriminator produces `ConfigError.MissingDiscriminatorKey`, which names the path and the key it expected:
340
+
341
+ ```scala
342
+ Config.load[Store](ConfigSource.fromMap(Map("backend.host" -> "localhost")))
343
+ // res18: Either[::[ConfigError], Store] = Left(
344
+ // List(MissingDiscriminatorKey(path = "backend", key = "type"))
345
+ // )
346
+ ```
347
+
348
+ An unrecognized discriminator produces `ConfigError.UnknownDiscriminator`, which lists every accepted value in sorted order — the error tells the reader what to write instead:
349
+
350
+ ```scala
351
+ Config.load[Store](ConfigSource.fromMap(Map("backend.type" -> "MySql")))
352
+ // res19: Either[::[ConfigError], Store] = Left(
353
+ // List(
354
+ // UnknownDiscriminator(
355
+ // path = "backend.type",
356
+ // found = "MySql",
357
+ // expected = List("Postgres", "Sqlite")
358
+ // )
359
+ // )
360
+ // )
361
+ ```
362
+
363
+ Case names are matched exactly as the schema reports them, which for a plain sealed trait is the simple class name.
364
+
365
+ ### Wrappers
366
+
367
+ A wrapper type — one whose schema is a `Binding.Wrapper`, such as a newtype over `String` — decodes its underlying representation and then applies the wrapping function. If wrapping throws, the failure is reported as `ConfigError.InvalidValue` at the wrapper's path, with the thrown exception as the cause. This is how validating newtypes surface their validation failures as ordinary config errors.
368
+
369
+ ### Dynamic Values
370
+
371
+ A `DynamicValue` field is not parsed at all. A present key becomes a `DynamicValue.Primitive` holding the raw string, and an absent key becomes `DynamicValue.Null` rather than an error. Use it for config sections whose shape is not known at compile time.
372
+
373
+ ## Customizing Derivation
374
+
375
+ `ConfigDecoderDeriver` is an ordinary `Deriver[ConfigDecoder]`, so the schema layer's override mechanisms apply. Two customizations are specific to config decoding.
376
+
377
+ ### Choosing a Discriminator Key
378
+
379
+ `ConfigDecoderDeriver#discriminator` returns a new deriver that reads a different discriminator key. Pass the deriver to `Config.load` to use it:
380
+
381
+ ```scala
382
+ import zio.blocks.config._
383
+ import zio.blocks.schema.Schema
384
+
385
+ sealed trait Backend
386
+
387
+ object Backend {
388
+ case class Postgres(host: String, port: Int) extends Backend
389
+ case class Sqlite(path: String) extends Backend
390
+
391
+ implicit val schema: Schema[Backend] = Schema.derived[Backend]
392
+ }
393
+
394
+ case class Store(backend: Backend)
395
+
396
+ object Store {
397
+ implicit val schema: Schema[Store] = Schema.derived[Store]
398
+ }
399
+
400
+ val kindDeriver = ConfigDecoderDeriver.discriminator("kind")
401
+ ```
402
+
403
+ The same config now selects its case from `backend.kind`:
404
+
405
+ ```scala
406
+ Config.load[Store](
407
+ ConfigSource.fromMap(Map("backend.kind" -> "Sqlite", "backend.path" -> "/tmp/db.sqlite")),
408
+ kindDeriver
409
+ )
410
+ // res21: Either[::[ConfigError], Store] = Right(
411
+ // Store(Sqlite("/tmp/db.sqlite"))
412
+ // )
413
+ ```
414
+
415
+ `ConfigDecoderDeriver` — the object — is the default instance, equivalent to `new ConfigDecoderDeriver("type")`.
416
+
417
+ ### Overriding a Single Type
418
+
419
+ To change how one type decodes while leaving everything else alone, build the decoder through the schema's `deriving` API and supply an instance override. This is the standard `Deriver` workflow described in the schema module's derivation documentation; the only config-specific part is that the type class being overridden is `ConfigDecoder`.
420
+
421
+ ## Error Accumulation
422
+
423
+ Decoding does not stop at the first failure. A record decoder attempts every field, collects the errors, and only then decides the outcome. The exact shape of the `Left` depends on how many errors it finds: a single error is returned on its own, and two or more are wrapped in one `ConfigError.Composite`.
424
+
425
+ That wrapping happens per record, so a nested config produces nested composites — one per record that had more than one failing field:
426
+
427
+ ```scala
428
+ import zio.blocks.config._
429
+ import zio.blocks.schema.Schema
430
+
431
+ case class Db(host: String, port: Int)
432
+
433
+ object Db {
434
+ implicit val schema: Schema[Db] = Schema.derived[Db]
435
+ }
436
+ ```
437
+
438
+ An empty source fails both fields, so both errors arrive together:
439
+
440
+ ```scala
441
+ Config.load[Db](ConfigSource.fromMap(Map.empty[String, String], "empty"))
442
+ // res23: Either[::[ConfigError], Db] = Left(
443
+ // List(
444
+ // Composite(
445
+ // List(
446
+ // MissingKey(path = "host", source = "empty"),
447
+ // MissingKey(path = "port", source = "empty")
448
+ // )
449
+ // )
450
+ // )
451
+ // )
452
+ ```
453
+
454
+ `ConfigError.Composite#message` joins the child messages with newlines, and `Config.loadOrThrow` formats them into a numbered report. See [Errors](./errors.md) for the full error model.
455
+
456
+ ## Integration Points
457
+
458
+ `ConfigDecoder` sits between the schema module and the source layer. It depends on `Schema[A]` and the `Deriver` machinery from `zio-blocks-schema`, reads through `ConfigSource`, and reports failures as `ConfigError`. `Config` is the user-facing wrapper, and `Config.wire` places a derived decoder inside a `zio-blocks-scope` dependency graph.
459
+
460
+ See [ConfigSource](./config-source.md) for building the sources a decoder reads from, and [Errors](./errors.md) for handling what it returns.