@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.
- package/adr/2026-07-18-data-migration.md +123 -0
- package/guides/async-getting-started.md +687 -0
- package/guides/compile-time-resource-safety-with-scope.md +6 -0
- package/guides/getting-started-with-mux.md +0 -112
- package/guides/query-dsl-extending.md +1 -1
- package/guides/query-dsl-fluent-builder.md +1 -1
- package/guides/query-dsl-reified-optics.md +1 -1
- package/guides/query-dsl-sql.md +395 -1
- package/guides/sql-checked-interpolation.md +173 -0
- package/guides/sql-transactions.md +286 -0
- package/guides/telemetry-guide.md +131 -70
- package/guides/zio-schema-migration.md +6 -6
- package/index.md +200 -583
- package/package.json +1 -1
- package/reference/async.md +1379 -531
- package/reference/chunk.md +3 -3
- package/reference/codegen/index.md +1 -1
- package/reference/combinators.md +4 -4
- package/reference/config/config-decoder.md +460 -0
- package/reference/config/config-source.md +489 -0
- package/reference/config/errors.md +278 -0
- package/reference/config/flags.md +369 -0
- package/reference/config/formats.md +314 -0
- package/reference/config/index.md +304 -0
- package/reference/config/rollout.md +336 -0
- package/reference/context.md +6 -49
- package/reference/data-migration.md +269 -0
- package/reference/datastar/attributes.md +302 -0
- package/reference/datastar/events.md +234 -0
- package/reference/datastar/index.md +256 -0
- package/reference/datastar/signals.md +230 -0
- package/reference/datastar/sse.md +295 -0
- package/reference/datastar.md +2 -2
- package/reference/docs.md +2 -2
- package/reference/endpoint/bulk-creation.md +96 -0
- package/reference/endpoint/index.md +9 -89
- package/reference/endpoint/path-codec.md +12 -24
- package/reference/endpoint/route-pattern.md +4 -6
- package/reference/endpoint/segment-codec.md +19 -32
- package/reference/html.md +313 -9
- package/reference/htmx/index.md +4 -52
- package/reference/htmx/response-headers.md +240 -0
- package/reference/http-model/headers.md +735 -0
- package/reference/http-model/index.md +3 -1
- package/reference/http-model/model.md +107 -71
- package/reference/http-model/schema-codecs.md +522 -0
- package/reference/http-model/schema.md +6 -3
- package/reference/http-model/server-sent-event.md +341 -0
- package/reference/jwt.md +195 -0
- package/reference/maybe.md +128 -11
- package/reference/media-type.md +2 -2
- package/reference/mux.md +254 -0
- package/reference/mux.mdx +7 -2
- package/reference/openapi.md +3 -3
- package/reference/projection.md +654 -0
- package/reference/resource-management/resource.md +2 -98
- package/reference/resource-management/scope.md +1 -209
- package/reference/resource-management/wire.md +4 -50
- package/reference/ringbuffer/advanced.mdx +1 -1
- package/reference/ringbuffer/index.mdx +3 -3
- package/reference/ringbuffer/mpmc.mdx +38 -4
- package/reference/ringbuffer/mpsc.mdx +36 -4
- package/reference/ringbuffer/spmc.mdx +1 -1
- package/reference/ringbuffer/spsc.mdx +87 -15
- package/reference/schema/allows.md +0 -96
- package/reference/schema/binding.md +2 -2
- package/reference/schema/built-in-codecs/avro.md +2 -2
- package/reference/schema/built-in-codecs/bson.md +50 -20
- package/reference/schema/built-in-codecs/csv.md +2 -2
- package/reference/schema/built-in-codecs/index.md +3 -3
- package/reference/schema/built-in-codecs/json/index.md +2 -2
- package/reference/schema/built-in-codecs/messagepack.md +3 -3
- package/reference/schema/built-in-codecs/thrift.md +2 -2
- package/reference/schema/built-in-codecs/toon.md +3 -3
- package/reference/schema/built-in-codecs/yaml.md +2 -2
- package/reference/schema/codec.md +11 -11
- package/reference/schema/dynamic-optic.md +48 -3
- package/reference/schema/dynamic-schema.md +3 -3
- package/reference/schema/index.md +2 -0
- package/reference/schema/path-interpolator.md +2 -0
- package/reference/schema/reflect-transformer.md +140 -0
- package/reference/schema/schema-evolution/as.md +4 -4
- package/reference/schema/schema-evolution/into.md +2 -2
- package/reference/schema/schema-expr.md +2 -2
- package/reference/schema/schema-search.md +263 -0
- package/reference/schema/schema.md +10 -2
- package/reference/schema/type-class-derivation.md +1 -1
- package/reference/smithy.md +502 -3
- package/reference/sql/db-codec-deriver.md +3 -3
- package/reference/sql/db-codec.md +22 -22
- package/reference/sql/db-con.md +4 -4
- package/reference/sql/db-connection.md +1 -1
- package/reference/sql/db-param.md +1 -1
- package/reference/sql/db-result-reader.md +4 -2
- package/reference/sql/db-tx.md +46 -14
- package/reference/sql/ddl.md +1 -1
- package/reference/sql/frag.md +44 -10
- package/reference/sql/index.md +7 -7
- package/reference/sql/repo.md +15 -15
- package/reference/sql/sql-dialect.md +1 -1
- package/reference/sql/sql-logger.md +1 -1
- package/reference/sql/sql-name-mapper.md +3 -3
- package/reference/sql/table-metadata.md +3 -3
- package/reference/sql/table.md +10 -10
- package/reference/sql/transactor-zio.md +1 -1
- package/reference/sql/transactor.md +21 -11
- package/reference/sql-zio.md +1 -1
- package/reference/streams/core/index.md +32 -0
- package/reference/streams/{pipeline.md → core/pipeline.md} +210 -74
- package/reference/streams/{sink.md → core/sink.md} +331 -353
- package/reference/streams/{stream.md → core/stream.md} +919 -209
- package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
- package/reference/streams/execution-and-compatibility/index.md +35 -0
- package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
- package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
- package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
- package/reference/streams/index.md +140 -67
- package/reference/streams/primitives/index.md +30 -0
- package/reference/streams/primitives/reader.md +1992 -0
- package/reference/streams/{writer.md → primitives/writer.md} +254 -98
- package/reference/telemetry/common/any-value.md +90 -0
- package/reference/telemetry/common/attribute-key.md +87 -0
- package/reference/telemetry/common/attributes.md +118 -0
- package/reference/telemetry/common/index.md +39 -0
- package/reference/telemetry/common/instrumentation-scope.md +24 -0
- package/reference/telemetry/common/resource.md +34 -0
- package/reference/telemetry/index.md +311 -0
- package/reference/telemetry/logging/index.md +197 -0
- package/reference/telemetry/logging/log-enrichment.md +72 -0
- package/reference/telemetry/logging/log-formatter.md +100 -0
- package/reference/telemetry/logging/log-record-processor.md +56 -0
- package/reference/telemetry/logging/log-record.md +44 -0
- package/reference/telemetry/logging/log-writer.md +64 -0
- package/reference/telemetry/logging/logger-provider.md +142 -0
- package/reference/telemetry/logging/logger.md +83 -0
- package/reference/telemetry/logging/severity.md +62 -0
- package/reference/telemetry/metrics/index.md +150 -0
- package/reference/telemetry/metrics/instruments.md +183 -0
- package/reference/telemetry/metrics/labeled-instruments.md +74 -0
- package/reference/telemetry/metrics/meter-provider.md +76 -0
- package/reference/telemetry/metrics/meter.md +98 -0
- package/reference/telemetry/metrics/metric-data.md +57 -0
- package/reference/telemetry/otel/custom-exporter.md +216 -0
- package/reference/telemetry/otel/index.md +212 -0
- package/reference/telemetry/tracing/index.md +155 -0
- package/reference/telemetry/tracing/sampler.md +89 -0
- package/reference/telemetry/tracing/span-builder.md +57 -0
- package/reference/telemetry/tracing/span-context.md +39 -0
- package/reference/telemetry/tracing/span-data.md +32 -0
- package/reference/telemetry/tracing/span-kind.md +55 -0
- package/reference/telemetry/tracing/span-processor.md +53 -0
- package/reference/telemetry/tracing/span-status.md +47 -0
- package/reference/telemetry/tracing/span.md +117 -0
- package/reference/telemetry/tracing/tracer-provider.md +91 -0
- package/reference/telemetry/tracing/tracer.md +52 -0
- package/reference/typeid.md +0 -64
- package/sidebars.js +150 -12
- package/undocumented-report.md +528 -270
- package/reference/config.md +0 -158
- package/reference/streams/concurrent-operators.md +0 -106
- package/reference/streams/reader.md +0 -1284
- package/reference/streams/scala-2-compatibility.md +0 -55
- package/reference/streams/zero-boxing.md +0 -275
- package/reference/telemetry.md +0 -693
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: schema-codecs
|
|
3
|
+
title: "HeaderCodec and QueryCodec"
|
|
4
|
+
sidebar_label: "Codecs"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
`HeaderCodec[A]` and `QueryCodec[A]` encode a whole value of type `A` into `Headers` or `QueryParams` and decode it back. Both are derived from `Schema[A]` through a format — `DefaultHeaderFormat` or `DefaultQueryFormat` — which pairs a MIME type with the `Deriver` that builds instances. The types involved:
|
|
8
|
+
|
|
9
|
+
```scala
|
|
10
|
+
abstract class HeaderCodec[A] extends Codec[Headers, HeadersBuilder, A] {
|
|
11
|
+
def encodeToHeaders(value: A): Headers
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
abstract class QueryCodec[A] extends Codec[QueryParams, QueryParamsBuilder, A] {
|
|
15
|
+
def encodeToQueryParams(value: A): QueryParams
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
abstract class HeaderFormat[TC[A] <: HeaderCodec[A]](val mimeType: String, val deriver: Deriver[TC])
|
|
19
|
+
abstract class QueryFormat[TC[A] <: QueryCodec[A]](val mimeType: String, val deriver: Deriver[TC])
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Motivation
|
|
23
|
+
|
|
24
|
+
The extension classes on [the schema page](./schema.md) read one field at a time: `request.query[Int]("page")` pulls a single parameter and gives you an `Either`. That is the right shape when a handler wants two or three values out of a request.
|
|
25
|
+
|
|
26
|
+
It is the wrong shape when the request *is* a value. A search endpoint taking eight query parameters becomes eight extractions, eight error branches, and a constructor call that has to be kept in step with all of them. Adding a parameter means touching four places.
|
|
27
|
+
|
|
28
|
+
A codec collapses that to one call. Define the case class, derive a `QueryCodec` from its schema, and `QueryCodec#decode` gives you `Either[SchemaError, SearchRequest]` for the whole thing. Adding a field to the case class adds a parameter, with no other change — the same bargain `Schema` makes everywhere else in the library.
|
|
29
|
+
|
|
30
|
+
The two directions are symmetric, which matters for clients as much as servers: the same codec that decodes an incoming request encodes an outgoing one.
|
|
31
|
+
|
|
32
|
+
## Quick Showcase
|
|
33
|
+
|
|
34
|
+
Derive a codec from a schema, then round-trip a value through it:
|
|
35
|
+
|
|
36
|
+
```scala
|
|
37
|
+
import zio.blocks.schema.Schema
|
|
38
|
+
import zio.http.schema._
|
|
39
|
+
|
|
40
|
+
final case class Search(page: Int, term: String, active: Boolean)
|
|
41
|
+
|
|
42
|
+
object Search {
|
|
43
|
+
implicit val schema: Schema[Search] = Schema.derived[Search]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
val codec = Schema[Search].derive(DefaultQueryFormat)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Encoding produces one parameter per field, named after the field:
|
|
50
|
+
|
|
51
|
+
```scala
|
|
52
|
+
codec.encodeToQueryParams(Search(2, "boots", active = true))
|
|
53
|
+
// res0: QueryParams = QueryParams((page,2), (term,boots), (active,true))
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Decoding is the inverse, and reports a `SchemaError` rather than throwing:
|
|
57
|
+
|
|
58
|
+
```scala
|
|
59
|
+
codec.decode(codec.encodeToQueryParams(Search(2, "boots", active = true)))
|
|
60
|
+
// res1: Either[SchemaError, Search] = Right(
|
|
61
|
+
// Search(page = 2, term = "boots", active = true)
|
|
62
|
+
// )
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Deriving a Codec
|
|
66
|
+
|
|
67
|
+
Both codecs are derived the same way: call `Schema#derive` with the format object. The implicit `Derivable` that each format supplies is what lets a format stand in for its deriver:
|
|
68
|
+
|
|
69
|
+
```scala
|
|
70
|
+
trait Schema[A] {
|
|
71
|
+
def derive[D, TC[_]](d: D)(implicit ev: Derivable[D, TC]): TC[A]
|
|
72
|
+
def deriving[TC[_]](deriver: Deriver[TC]): DerivationBuilder[TC, A]
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`DefaultQueryFormat` produces a `QueryCodec`, and `DefaultHeaderFormat` produces a `HeaderCodec`:
|
|
77
|
+
|
|
78
|
+
```scala
|
|
79
|
+
import zio.blocks.schema.Schema
|
|
80
|
+
import zio.http.schema._
|
|
81
|
+
|
|
82
|
+
final case class Trace(traceId: String, apiKey: String)
|
|
83
|
+
|
|
84
|
+
object Trace {
|
|
85
|
+
implicit val schema: Schema[Trace] = Schema.derived[Trace]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
val headerCodec = Schema[Trace].derive(DefaultHeaderFormat)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The derived codec carries the format's MIME type, which is what an endpoint description uses to advertise the wire shape:
|
|
92
|
+
|
|
93
|
+
```scala
|
|
94
|
+
DefaultHeaderFormat.mimeType
|
|
95
|
+
// res3: String = "application/http-headers"
|
|
96
|
+
DefaultQueryFormat.mimeType
|
|
97
|
+
// res4: String = "application/x-www-form-urlencoded"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Encoding and Decoding
|
|
101
|
+
|
|
102
|
+
Each codec has a convenience encoder that returns the finished collection, plus the `Codec#encode` and `Codec#decode` methods it inherits.
|
|
103
|
+
|
|
104
|
+
### `HeaderCodec#encodeToHeaders` and `QueryCodec#encodeToQueryParams`
|
|
105
|
+
|
|
106
|
+
These build and return the collection directly, which is what application code normally wants:
|
|
107
|
+
|
|
108
|
+
```scala
|
|
109
|
+
abstract class HeaderCodec[A] {
|
|
110
|
+
def encodeToHeaders(value: A): Headers
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Encoding a record yields one entry per field:
|
|
115
|
+
|
|
116
|
+
```scala
|
|
117
|
+
headerCodec.encodeToHeaders(Trace("trace-1", "secret"))
|
|
118
|
+
// res5: Headers = Headers(trace-id: trace-1, api-key: secret)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
:::warning[The builder is a reused thread-local]
|
|
122
|
+
`HeaderCodec#encodeToHeaders` and `QueryCodec#encodeToQueryParams` borrow a per-thread builder, reset it, fill it, and snapshot it. The returned collection is safe to keep, but the builder is not reentrant: if a custom codec's `Codec#encode` calls `HeaderCodec#encodeToHeaders` again on the same thread, the inner call resets the buffer the outer one was filling. Inside a custom `Codec#encode`, write to the `output` builder you were handed rather than calling the convenience encoder.
|
|
123
|
+
:::
|
|
124
|
+
|
|
125
|
+
### `Codec#encode` and `Codec#decode`
|
|
126
|
+
|
|
127
|
+
`Codec#encode` writes into a builder you supply, which is how nested and composed codecs avoid intermediate collections. `Codec#decode` reads a whole collection:
|
|
128
|
+
|
|
129
|
+
```scala
|
|
130
|
+
abstract class QueryCodec[A] {
|
|
131
|
+
def encode(value: A, output: QueryParamsBuilder): Unit
|
|
132
|
+
def decode(input: QueryParams): Either[SchemaError, A]
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
A decode failure names the field and what was expected:
|
|
137
|
+
|
|
138
|
+
```scala
|
|
139
|
+
import zio.blocks.schema.Schema
|
|
140
|
+
import zio.http.schema._
|
|
141
|
+
import zio.http.QueryParams
|
|
142
|
+
|
|
143
|
+
final case class Search(page: Int, term: String)
|
|
144
|
+
|
|
145
|
+
object Search {
|
|
146
|
+
implicit val schema: Schema[Search] = Schema.derived[Search]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
val codec = Schema[Search].derive(DefaultQueryFormat)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
A missing parameter and a malformed one are both reported as a `SchemaError`, not an exception:
|
|
153
|
+
|
|
154
|
+
```scala
|
|
155
|
+
codec.decode(QueryParams("term" -> "boots"))
|
|
156
|
+
// res7: Either[SchemaError, Search] = Left(
|
|
157
|
+
// SchemaError(
|
|
158
|
+
// List(MissingField(source = DynamicOptic(ArraySeq()), fieldName = "page"))
|
|
159
|
+
// )
|
|
160
|
+
// )
|
|
161
|
+
codec.decode(QueryParams("page" -> "not-a-number", "term" -> "boots"))
|
|
162
|
+
// res8: Either[SchemaError, Search] = Left(
|
|
163
|
+
// SchemaError(
|
|
164
|
+
// List(
|
|
165
|
+
// ConversionFailed(
|
|
166
|
+
// source = DynamicOptic(ArraySeq()),
|
|
167
|
+
// details = "Malformed query parameter 'page' value 'not-a-number': Cannot parse 'not-a-number' as Int",
|
|
168
|
+
// cause = None
|
|
169
|
+
// )
|
|
170
|
+
// )
|
|
171
|
+
// )
|
|
172
|
+
// )
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Field Naming
|
|
176
|
+
|
|
177
|
+
The two codecs disagree about names, and this is the single most important thing to know about them.
|
|
178
|
+
|
|
179
|
+
| | Field `traceId` becomes | Decoding |
|
|
180
|
+
| ------------- | ----------------------- | ---------------- |
|
|
181
|
+
| `QueryCodec` | `traceId` — verbatim | exact match |
|
|
182
|
+
| `HeaderCodec` | `trace-id` — kebab-case | case-insensitive |
|
|
183
|
+
|
|
184
|
+
`QueryCodec` uses the Scala field name unchanged, because query parameters are conventionally camelCase or snake_case and the schema's name is as good a guess as any.
|
|
185
|
+
|
|
186
|
+
`HeaderCodec` rewrites camelCase into kebab-case, because that is the conventional shape of an HTTP header name. Two steps are involved: the deriver emits `TRACE-ID`, uppercasing every character and inserting a hyphen before each interior capital, and then `Headers` lowercases every name as it stores it. The second step makes the first unobservable, so what you get back is `trace-id`:
|
|
187
|
+
|
|
188
|
+
```scala
|
|
189
|
+
import zio.blocks.schema.Schema
|
|
190
|
+
import zio.http.schema._
|
|
191
|
+
import zio.http.Headers
|
|
192
|
+
|
|
193
|
+
final case class Meta(traceId: String, bigIntValue: BigInt, uuidValue: java.util.UUID)
|
|
194
|
+
|
|
195
|
+
object Meta {
|
|
196
|
+
implicit val schema: Schema[Meta] = Schema.derived[Meta]
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
val codec = Schema[Meta].derive(DefaultHeaderFormat)
|
|
200
|
+
val uuid = java.util.UUID.fromString("123e4567-e89b-12d3-a456-426614174000")
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Every camelCase boundary becomes a hyphen, so `bigIntValue` becomes three segments rather than two:
|
|
204
|
+
|
|
205
|
+
```scala
|
|
206
|
+
codec.encodeToHeaders(Meta("trace-1", BigInt(42), uuid)).toList.map(_._1)
|
|
207
|
+
// res10: List[String] = List("trace-id", "big-int-value", "uuid-value")
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Decoding ignores case, so a client that sends `trace-id` or `TRACE-ID` still decodes:
|
|
211
|
+
|
|
212
|
+
```scala
|
|
213
|
+
codec.decode(Headers("trace-id" -> "t", "big-int-value" -> "42", "UUID-VALUE" -> uuid.toString))
|
|
214
|
+
// res11: Either[SchemaError, Meta] = Right(
|
|
215
|
+
// Meta(
|
|
216
|
+
// traceId = "t",
|
|
217
|
+
// bigIntValue = 42,
|
|
218
|
+
// uuidValue = 123e4567-e89b-12d3-a456-426614174000
|
|
219
|
+
// )
|
|
220
|
+
// )
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
:::warning[Names are not configurable]
|
|
224
|
+
Neither codec offers a name mapper. The transformation is fixed, so a header or parameter whose wire name does not follow the convention cannot be reached by renaming the field — you need a custom codec instance for that type, or the field-at-a-time extraction on [the schema page](./schema.md).
|
|
225
|
+
:::
|
|
226
|
+
|
|
227
|
+
## Supported Field Shapes
|
|
228
|
+
|
|
229
|
+
Four shapes are supported per field, and they compose one level deep.
|
|
230
|
+
|
|
231
|
+
### Primitives
|
|
232
|
+
|
|
233
|
+
Each primitive field is one entry, rendered as its string form:
|
|
234
|
+
|
|
235
|
+
| Scala type | Example wire value |
|
|
236
|
+
| --- | --- |
|
|
237
|
+
| `String` | `alice` |
|
|
238
|
+
| `Boolean` | `true` |
|
|
239
|
+
| `Byte`, `Short`, `Int`, `Long` | `42` |
|
|
240
|
+
| `Float`, `Double` | `12.34` |
|
|
241
|
+
| `BigInt`, `BigDecimal` | `1234567890123456789` |
|
|
242
|
+
| `Char` | `z` — exactly one character |
|
|
243
|
+
| `UUID` | `123e4567-e89b-12d3-a456-426614174000` |
|
|
244
|
+
|
|
245
|
+
A `Char` field with more than one character fails to decode, and the message says so:
|
|
246
|
+
|
|
247
|
+
```scala
|
|
248
|
+
import zio.blocks.schema.Schema
|
|
249
|
+
import zio.http.schema._
|
|
250
|
+
import zio.http.QueryParams
|
|
251
|
+
|
|
252
|
+
val charCodec = Schema[Char].derive(DefaultQueryFormat)
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
The error names the expectation rather than the underlying exception:
|
|
256
|
+
|
|
257
|
+
```scala
|
|
258
|
+
charCodec.decode(QueryParams("value" -> "too-long"))
|
|
259
|
+
// res13: Either[SchemaError, Char] = Left(
|
|
260
|
+
// SchemaError(
|
|
261
|
+
// List(
|
|
262
|
+
// ConversionFailed(
|
|
263
|
+
// source = DynamicOptic(ArraySeq()),
|
|
264
|
+
// details = "Malformed query parameter 'value' value 'too-long': Expected single character but got 'too-long'",
|
|
265
|
+
// cause = None
|
|
266
|
+
// )
|
|
267
|
+
// )
|
|
268
|
+
// )
|
|
269
|
+
// )
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Optional Fields
|
|
273
|
+
|
|
274
|
+
An `Option[A]` field omits its key entirely when `None`, and an absent key decodes back to `None`:
|
|
275
|
+
|
|
276
|
+
```scala
|
|
277
|
+
import zio.blocks.schema.Schema
|
|
278
|
+
import zio.http.schema._
|
|
279
|
+
import zio.http.QueryParams
|
|
280
|
+
|
|
281
|
+
final case class Filters(page: Option[Int], term: Option[String])
|
|
282
|
+
|
|
283
|
+
object Filters {
|
|
284
|
+
implicit val schema: Schema[Filters] = Schema.derived[Filters]
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
val codec = Schema[Filters].derive(DefaultQueryFormat)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Encoding drops the absent field rather than emitting an empty value:
|
|
291
|
+
|
|
292
|
+
```scala
|
|
293
|
+
codec.encodeToQueryParams(Filters(Some(7), None))
|
|
294
|
+
// res15: QueryParams = QueryParams((page,7))
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
An empty collection decodes to all-`None`, so a request with no parameters is valid rather than an error:
|
|
298
|
+
|
|
299
|
+
```scala
|
|
300
|
+
codec.decode(QueryParams.empty)
|
|
301
|
+
// res16: Either[SchemaError, Filters] = Right(
|
|
302
|
+
// Filters(page = None, term = None)
|
|
303
|
+
// )
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Sequences
|
|
307
|
+
|
|
308
|
+
A `List`, `Chunk`, or other sequence field becomes **repeated entries under one key**, not a delimited single value:
|
|
309
|
+
|
|
310
|
+
```scala
|
|
311
|
+
import zio.blocks.schema.Schema
|
|
312
|
+
import zio.blocks.chunk.Chunk
|
|
313
|
+
import zio.http.schema._
|
|
314
|
+
|
|
315
|
+
final case class Selection(tags: List[String], ids: Chunk[Int])
|
|
316
|
+
|
|
317
|
+
object Selection {
|
|
318
|
+
implicit val schema: Schema[Selection] = Schema.derived[Selection]
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
val codec = Schema[Selection].derive(DefaultQueryFormat)
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Two tags and three ids produce five parameters:
|
|
325
|
+
|
|
326
|
+
```scala
|
|
327
|
+
codec.encodeToQueryParams(Selection(List("a", "b"), Chunk(1, 2, 3)))
|
|
328
|
+
// res18: QueryParams = QueryParams((tags,a), (tags,b), (ids,1), (ids,2), (ids,3))
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Decoding collects them back into the declared collection type, preserving order:
|
|
332
|
+
|
|
333
|
+
```scala
|
|
334
|
+
codec.decode(codec.encodeToQueryParams(Selection(List("a", "b"), Chunk(1, 2, 3))))
|
|
335
|
+
// res19: Either[SchemaError, Selection] = Right(
|
|
336
|
+
// Selection(tags = List("a", "b"), ids = IndexedSeq(1, 2, 3))
|
|
337
|
+
// )
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Because there is no delimiter, a value containing a comma round-trips correctly — the repeated-key encoding has no escaping problem to solve.
|
|
341
|
+
|
|
342
|
+
### Wrapper Types
|
|
343
|
+
|
|
344
|
+
A newtype built with `Schema#transform` encodes as its underlying value, so the wrapper is invisible on the wire:
|
|
345
|
+
|
|
346
|
+
```scala
|
|
347
|
+
import zio.blocks.schema.Schema
|
|
348
|
+
import zio.http.schema._
|
|
349
|
+
|
|
350
|
+
final case class UserId(value: String)
|
|
351
|
+
|
|
352
|
+
object UserId {
|
|
353
|
+
implicit val schema: Schema[UserId] = Schema[String].transform(UserId(_), _.value)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
final case class Owned(id: UserId)
|
|
357
|
+
|
|
358
|
+
object Owned {
|
|
359
|
+
implicit val schema: Schema[Owned] = Schema.derived[Owned]
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
val codec = Schema[Owned].derive(DefaultQueryFormat)
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
The parameter holds the wrapped string, with no trace of the wrapper:
|
|
366
|
+
|
|
367
|
+
```scala
|
|
368
|
+
codec.encodeToQueryParams(Owned(UserId("user-1")))
|
|
369
|
+
// res21: QueryParams = QueryParams((id,user-1))
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
If the wrapping function throws — a validating newtype rejecting its input — the failure surfaces as a `SchemaError` from `QueryCodec#decode` rather than as an exception.
|
|
373
|
+
|
|
374
|
+
## Top-Level Codecs
|
|
375
|
+
|
|
376
|
+
A schema that is not a record also derives, and the resulting codec uses the single key `value`:
|
|
377
|
+
|
|
378
|
+
```scala
|
|
379
|
+
import zio.blocks.schema.Schema
|
|
380
|
+
import zio.http.schema._
|
|
381
|
+
import zio.http.{Headers, QueryParams}
|
|
382
|
+
|
|
383
|
+
val intCodec = Schema[Int].derive(DefaultQueryFormat)
|
|
384
|
+
val listCodec = Schema[List[Int]].derive(DefaultQueryFormat)
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
A top-level primitive is one parameter, and a top-level sequence is repeated parameters, both under `value`:
|
|
388
|
+
|
|
389
|
+
```scala
|
|
390
|
+
intCodec.encodeToQueryParams(42)
|
|
391
|
+
// res23: QueryParams = QueryParams((value,42))
|
|
392
|
+
listCodec.encodeToQueryParams(List(1, 2))
|
|
393
|
+
// res24: QueryParams = QueryParams((value,1), (value,2))
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
`HeaderCodec` applies its naming rule here too, so the single-word name comes back as `value` and is matched case-insensitively on the way in:
|
|
397
|
+
|
|
398
|
+
```scala
|
|
399
|
+
val headerIntCodec = Schema[Int].derive(DefaultHeaderFormat)
|
|
400
|
+
// headerIntCodec: HeaderCodec[Int] = zio.http.schema.HeaderCodecDeriver$$anon$3@1ceeab16
|
|
401
|
+
headerIntCodec.encodeToHeaders(42).toList
|
|
402
|
+
// res25: List[Tuple2[String, String]] = List(("value", "42"))
|
|
403
|
+
headerIntCodec.decode(Headers("value" -> "42"))
|
|
404
|
+
// res26: Either[SchemaError, Int] = Right(42)
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## Unsupported Shapes
|
|
408
|
+
|
|
409
|
+
Three shapes have no encoding, and one of them is a trap.
|
|
410
|
+
|
|
411
|
+
**Nested records** are rejected during derivation, because a flat namespace of names has no way to express a field whose value is itself a record. Use a flat case class, or extract the nested part separately.
|
|
412
|
+
|
|
413
|
+
**Top-level `Option`, `Map`, and `DynamicValue`** derive without complaint and then **throw when you encode**:
|
|
414
|
+
|
|
415
|
+
```scala
|
|
416
|
+
import zio.blocks.schema.Schema
|
|
417
|
+
import zio.http.schema._
|
|
418
|
+
|
|
419
|
+
val optionCodec = Schema[Option[Int]].derive(DefaultQueryFormat)
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Derivation succeeds, so nothing warns you at the point where the mistake was made:
|
|
423
|
+
|
|
424
|
+
```scala
|
|
425
|
+
scala.util.Try(optionCodec.encodeToQueryParams(Some(1))).isFailure
|
|
426
|
+
// res28: Boolean = true
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
:::danger[Failure is deferred to the first encode]
|
|
430
|
+
`Schema[Option[A]]`, `Schema[Map[K, V]]`, and `Schema[DynamicValue]` all produce a codec that throws on use. Because derivation is where the error belongs and is not where it appears, a codec built at startup can look healthy until the first request that encodes through it. Derive and exercise these codecs in a test rather than trusting that construction succeeded.
|
|
431
|
+
:::
|
|
432
|
+
|
|
433
|
+
A `Map` field inside a record is likewise unsupported. Model the entries you expect as fields, or take the raw collection and read it with the field-at-a-time API.
|
|
434
|
+
|
|
435
|
+
## Formats
|
|
436
|
+
|
|
437
|
+
A format is the pairing of a MIME type with a `Deriver`. `HeaderFormat` and `QueryFormat` are the base classes, and each provides an implicit `Derivable` so that `Schema#derive` accepts the format object directly:
|
|
438
|
+
|
|
439
|
+
```scala
|
|
440
|
+
abstract class HeaderFormat[TC[A] <: HeaderCodec[A]](val mimeType: String, val deriver: Deriver[TC]) {
|
|
441
|
+
implicit def derivable: Derivable[this.type, TC]
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
The two built-in formats are the only ones needed for the default wire shapes:
|
|
446
|
+
|
|
447
|
+
| Format | MIME type | Deriver |
|
|
448
|
+
| --- | --- | --- |
|
|
449
|
+
| `DefaultHeaderFormat` | `application/http-headers` | `HeaderCodecDeriver` |
|
|
450
|
+
| `DefaultQueryFormat` | `application/x-www-form-urlencoded` | `QueryCodecDeriver` |
|
|
451
|
+
|
|
452
|
+
Both are `case object`s extending a sealed abstract class, so they are singletons and can be matched on.
|
|
453
|
+
|
|
454
|
+
### Defining a Custom Format
|
|
455
|
+
|
|
456
|
+
Subclass `QueryFormat` or `HeaderFormat` when you want a different MIME type reported for the same derivation strategy, or a different deriver entirely:
|
|
457
|
+
|
|
458
|
+
```scala
|
|
459
|
+
import zio.http.schema._
|
|
460
|
+
|
|
461
|
+
case object FormUrlEncoded extends QueryFormat[QueryCodec]("application/x-www-form-urlencoded; charset=utf-8", QueryCodecDeriver)
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
The `TC` parameter is the codec type the deriver produces, bounded by `QueryCodec` so that the convenience encoder stays available on whatever it derives.
|
|
465
|
+
|
|
466
|
+
## Overriding a Single Type
|
|
467
|
+
|
|
468
|
+
`HeaderCodecDeriver` and `QueryCodecDeriver` are ordinary `Deriver` instances, so the schema module's override mechanism applies. Supply a hand-written codec for one type and let derivation handle the rest:
|
|
469
|
+
|
|
470
|
+
```scala
|
|
471
|
+
import zio.blocks.schema.{Schema, SchemaError}
|
|
472
|
+
import zio.blocks.typeid.TypeId
|
|
473
|
+
import zio.http.schema._
|
|
474
|
+
import zio.http.{QueryParams, QueryParamsBuilder}
|
|
475
|
+
|
|
476
|
+
val prefixedInt = new QueryCodec[Int] {
|
|
477
|
+
def encode(value: Int, output: QueryParamsBuilder): Unit =
|
|
478
|
+
output.add("value", s"custom-$value")
|
|
479
|
+
|
|
480
|
+
def decode(input: QueryParams): Either[SchemaError, Int] =
|
|
481
|
+
input.getFirst("value") match {
|
|
482
|
+
case Some(s) if s.startsWith("custom-") => Right(s.stripPrefix("custom-").toInt)
|
|
483
|
+
case other => Left(SchemaError(s"Expected custom- prefix, got: $other"))
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
val codec = Schema[Int].deriving(QueryCodecDeriver).instance(TypeId.int, prefixedInt).derive
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
The override applies wherever that type appears, so the custom rendering is used on both sides:
|
|
491
|
+
|
|
492
|
+
```scala
|
|
493
|
+
codec.encodeToQueryParams(42)
|
|
494
|
+
// res31: QueryParams = QueryParams((value,custom-42))
|
|
495
|
+
codec.decode(codec.encodeToQueryParams(42))
|
|
496
|
+
// res32: Either[SchemaError, Int] = Right(42)
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
Note that `Codec#encode` writes into the supplied builder rather than returning a collection — that is the method to implement, and the convenience encoder is derived from it.
|
|
500
|
+
|
|
501
|
+
## Choosing Between the Two APIs
|
|
502
|
+
|
|
503
|
+
Both this page's codecs and [the schema page](./schema.md)'s extension classes read the same collections. They differ in granularity:
|
|
504
|
+
|
|
505
|
+
| | Codecs | Extension classes |
|
|
506
|
+
| --- | --- | --- |
|
|
507
|
+
| Unit of work | Whole value | One field |
|
|
508
|
+
| Result | `Either[SchemaError, A]` | `Either[QueryParamError, T]` / `Either[HeaderError, T]` |
|
|
509
|
+
| Encoding | Yes, symmetric with decoding | Decoding only |
|
|
510
|
+
| Names | Fixed by the naming rule | You pass the name |
|
|
511
|
+
| Nested records | Unsupported | Not applicable |
|
|
512
|
+
| Best for | A request or response that *is* a value | Pulling two or three values out |
|
|
513
|
+
|
|
514
|
+
Use a codec when the collection maps onto a type you already have. Use the extension classes when it does not, when you need a name the codec cannot produce, or when you want per-field error handling.
|
|
515
|
+
|
|
516
|
+
## Integration Points
|
|
517
|
+
|
|
518
|
+
`HeaderCodec` and `QueryCodec` extend `Codec[Out, Builder, A]` from `zio-blocks-schema`, so they participate in the same derivation machinery as every other codec in the library — the `Deriver`, `Derivable`, and instance-override APIs are the schema module's, not this module's.
|
|
519
|
+
|
|
520
|
+
On the HTTP side they produce and consume `Headers`, `HeadersBuilder`, `QueryParams`, and `QueryParamsBuilder` from `zio-blocks-http-model`; see [the HTTP model](./model.md) for those collections and [Header](./headers.md) for the typed single-header model, which is a different mechanism from the whole-record codecs here.
|
|
521
|
+
|
|
522
|
+
Errors are `SchemaError` from the schema module rather than this module's `HeaderError` and `QueryParamError`, which belong to the field-at-a-time API on [the schema page](./schema.md).
|
|
@@ -7,6 +7,8 @@ title: "Schema-Based Typed Access"
|
|
|
7
7
|
|
|
8
8
|
Core features are built on **extension methods** — `QueryParamsSchemaOps`, `HeadersSchemaOps`, `RequestSchemaOps`, `ResponseSchemaOps` — which add typed, schema-based extraction to query parameters and headers. Complemented by error types `QueryParamError` and `HeaderError`, the module provides automatic decoding for 11 primitive types and extensible support for custom types via `Schema[T]`.
|
|
9
9
|
|
|
10
|
+
This page covers extraction one field at a time. The module also derives whole-value codecs — a `QueryCodec[A]` or `HeaderCodec[A]` that encodes and decodes an entire case class in one call — which are documented in [Codecs](./schema-codecs.md).
|
|
11
|
+
|
|
10
12
|
## Motivation
|
|
11
13
|
|
|
12
14
|
Building HTTP handlers often requires extracting and validating query parameters or headers — "get the `userId` query parameter as a `UUID`." Without schema-based extraction, this becomes tedious and error-prone:
|
|
@@ -52,13 +54,13 @@ This keeps HTTP request handling clean, testable, and portable across different
|
|
|
52
54
|
Add the following to your `build.sbt`:
|
|
53
55
|
|
|
54
56
|
```
|
|
55
|
-
libraryDependencies += "dev.zio" %% "zio-http-model-schema" % "0.0.
|
|
57
|
+
libraryDependencies += "dev.zio" %% "zio-http-model-schema" % "0.0.55"
|
|
56
58
|
```
|
|
57
59
|
|
|
58
60
|
For cross-platform projects (Scala.js):
|
|
59
61
|
|
|
60
62
|
```
|
|
61
|
-
libraryDependencies += "dev.zio" %%% "zio-http-model-schema" % "0.0.
|
|
63
|
+
libraryDependencies += "dev.zio" %%% "zio-http-model-schema" % "0.0.55"
|
|
62
64
|
```
|
|
63
65
|
|
|
64
66
|
Supported Scala versions: Scala 3.x only. Requires `zio-http-model` and `zio-blocks-schema` as dependencies.
|
|
@@ -740,8 +742,9 @@ Expected single character but got ''
|
|
|
740
742
|
|
|
741
743
|
### Custom Types
|
|
742
744
|
|
|
743
|
-
To support custom types, provide a `Schema[T]` instance. The module automatically uses the schema's primitive type information via `StringDecoder`. For case classes or other compound types,
|
|
745
|
+
To support custom types, provide a `Schema[T]` instance. The module automatically uses the schema's primitive type information via `StringDecoder`. For case classes or other compound types, derive a whole-value codec instead — see [Codecs](./schema-codecs.md), which maps a case class onto a whole collection of parameters or headers rather than a single value.
|
|
744
746
|
|
|
745
747
|
## See Also
|
|
746
748
|
|
|
749
|
+
- [Codecs](./schema-codecs.md) — `QueryCodec` and `HeaderCodec`, derived from a schema, for encoding and decoding a whole value rather than one field
|
|
747
750
|
- [Combinators](../combinators.md) — Systematically compose and canonicalize Either types for uniform error handling across multiple query parameter or header extractions. The `Eithers` combinator canonicalizes nested Either types, making error accumulation consistent.
|