@zio.dev/zio-blocks 0.0.33 → 0.0.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/guides/compile-time-resource-safety-with-scope.md +16 -17
- package/guides/getting-started-with-mux.md +1507 -0
- package/guides/query-dsl-extending.md +161 -102
- package/guides/query-dsl-fluent-builder.md +217 -157
- package/guides/query-dsl-reified-optics.md +12 -10
- package/guides/query-dsl-sql.md +246 -165
- package/guides/telemetry-guide.md +1069 -0
- package/guides/zio-schema-migration.md +29 -22
- package/index.md +292 -50
- package/package.json +1 -1
- package/plans/config-follow-up-prs.md +188 -0
- package/plans/config-pr-assessment-roadmap.md +310 -0
- package/reference/MuxDataFlow.jsx +250 -0
- package/reference/async.md +651 -0
- package/reference/chunk.md +3533 -308
- package/reference/codegen/case-class.md +436 -0
- package/reference/codegen/emitter-config.md +383 -0
- package/reference/codegen/examples.md +664 -0
- package/reference/codegen/field.md +316 -0
- package/reference/codegen/index.md +317 -0
- package/reference/codegen/scala-emitter.md +392 -0
- package/reference/codegen/scala-file.md +276 -0
- package/reference/codegen/sealed-trait.md +408 -0
- package/reference/codegen/type-definition.md +340 -0
- package/reference/codegen/type-ref.md +201 -0
- package/reference/combinators.md +347 -117
- package/reference/config.md +158 -0
- package/reference/context.md +4 -4
- package/reference/datastar.md +346 -0
- package/reference/docs.md +1461 -345
- package/reference/endpoint/auth-type.md +146 -0
- package/reference/endpoint/endpoint.md +297 -0
- package/reference/endpoint/http-codec.md +249 -0
- package/reference/endpoint/index.md +825 -0
- package/reference/endpoint/path-codec.md +237 -0
- package/reference/endpoint/route-pattern.md +196 -0
- package/reference/endpoint/route-tree.md +111 -0
- package/reference/endpoint/segment-codec.md +212 -0
- package/reference/html.md +1120 -0
- package/reference/htmx/attribute-values.md +359 -0
- package/reference/htmx/hx-encoding.md +111 -0
- package/reference/htmx/hx-params.md +204 -0
- package/reference/htmx/hx-swap.md +276 -0
- package/reference/htmx/hx-sync.md +251 -0
- package/reference/htmx/hx-target.md +314 -0
- package/reference/htmx/hx-trigger.md +457 -0
- package/reference/htmx/hx-url-update.md +239 -0
- package/reference/htmx/index.md +855 -0
- package/reference/http-model/index.md +47 -0
- package/reference/http-model/model.md +1481 -0
- package/reference/http-model/schema.md +747 -0
- package/reference/maybe.md +826 -0
- package/reference/media-type.md +2 -2
- package/reference/mux.mdx +823 -0
- package/reference/openapi.md +1351 -0
- package/reference/resource-management/defer-handle.md +1 -1
- package/reference/resource-management/resource.md +31 -2
- package/reference/resource-management/scope.md +28 -12
- package/reference/resource-management/wire.md +3 -7
- package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
- package/reference/ringbuffer/MpscDiagram.jsx +618 -0
- package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
- package/reference/ringbuffer/SpscDiagram.jsx +677 -0
- package/reference/ringbuffer/advanced.mdx +109 -0
- package/reference/ringbuffer/index.mdx +145 -0
- package/reference/ringbuffer/mpmc.mdx +151 -0
- package/reference/ringbuffer/mpsc.mdx +132 -0
- package/reference/ringbuffer/spmc.mdx +108 -0
- package/reference/ringbuffer/spsc.mdx +344 -0
- package/reference/{allows.md → schema/allows.md} +4 -4
- package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
- package/reference/{binding.md → schema/binding.md} +2 -3
- package/reference/schema/built-in-codecs/avro.md +451 -0
- package/reference/schema/built-in-codecs/bson.md +480 -0
- package/reference/schema/built-in-codecs/csv.md +564 -0
- package/reference/schema/built-in-codecs/index.md +77 -0
- package/reference/schema/built-in-codecs/json/index.md +295 -0
- package/reference/schema/built-in-codecs/json/json-config.md +217 -0
- package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
- package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
- package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
- package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
- package/reference/schema/built-in-codecs/messagepack.md +508 -0
- package/reference/schema/built-in-codecs/thrift.md +433 -0
- package/reference/schema/built-in-codecs/toon.md +1078 -0
- package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
- package/reference/schema/built-in-codecs/yaml.md +552 -0
- package/reference/{codec.md → schema/codec.md} +10 -10
- package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +151 -5
- package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
- package/reference/schema/format.md +92 -0
- package/reference/schema/index.md +50 -0
- package/reference/schema/migration.md +297 -0
- package/reference/{modifier.md → schema/modifier.md} +58 -7
- package/reference/{optics.md → schema/optics.md} +2 -2
- package/reference/{patch.md → schema/patch.md} +1 -1
- package/{path-interpolator.md → reference/schema/path-interpolator.md} +165 -72
- package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
- package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
- package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
- package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
- package/reference/{schema.md → schema/schema.md} +12 -0
- package/reference/{structural-types.md → schema/structural-types.md} +1 -1
- package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
- package/reference/smithy.md +533 -0
- package/reference/sql/db-codec-deriver.md +71 -0
- package/reference/sql/db-codec.md +687 -0
- package/reference/sql/db-con.md +271 -0
- package/reference/sql/db-connection.md +153 -0
- package/reference/sql/db-param-writer.md +77 -0
- package/reference/sql/db-param.md +66 -0
- package/reference/sql/db-result-reader.md +146 -0
- package/reference/sql/db-tx.md +82 -0
- package/reference/sql/db-value.md +41 -0
- package/reference/sql/ddl.md +85 -0
- package/reference/sql/frag.md +254 -0
- package/reference/sql/index.md +341 -0
- package/reference/sql/repo.md +600 -0
- package/reference/sql/sql-dialect.md +73 -0
- package/reference/sql/sql-logger.md +62 -0
- package/reference/sql/sql-name-mapper.md +70 -0
- package/reference/sql/table-metadata.md +134 -0
- package/reference/sql/table.md +448 -0
- package/reference/sql/transactor-zio.md +399 -0
- package/reference/sql/transactor.md +353 -0
- package/reference/sql-zio.md +112 -0
- package/reference/streams/concurrent-operators.md +106 -0
- package/reference/streams/index.md +653 -0
- package/reference/streams/pipeline.md +718 -0
- package/reference/streams/reader.md +1284 -0
- package/reference/streams/scala-2-compatibility.md +55 -0
- package/reference/streams/sink.md +1426 -0
- package/reference/streams/stream.md +2526 -0
- package/reference/streams/writer.md +1045 -0
- package/reference/streams/zero-boxing.md +275 -0
- package/reference/telemetry.md +693 -0
- package/reference/typeid.md +5 -19
- package/sidebars.js +238 -43
- package/reference/formats.md +0 -694
- package/reference/http-model.md +0 -1716
- package/reference/streams.md +0 -989
- package/ringbuffer.md +0 -249
- /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
- /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
- /package/reference/{lazy.md → schema/lazy.md} +0 -0
- /package/reference/{reflect.md → schema/reflect.md} +0 -0
- /package/reference/{registers.md → schema/registers.md} +0 -0
- /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
- /package/reference/{syntax.md → schema/syntax.md} +0 -0
- /package/reference/{validation.md → schema/validation.md} +0 -0
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: schema
|
|
3
|
+
title: "Schema-Based Typed Access"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`zio-http-model-schema` adds **type-safe, validated extraction** of query parameters and headers to the core HTTP model. It provides extension methods on `QueryParams`, `Headers`, `Request`, and `Response` that automatically decode string values to typed objects using schema-based decoding with comprehensive error reporting.
|
|
7
|
+
|
|
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
|
+
|
|
10
|
+
## Motivation
|
|
11
|
+
|
|
12
|
+
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:
|
|
13
|
+
|
|
14
|
+
```scala
|
|
15
|
+
import zio.http.QueryParams
|
|
16
|
+
|
|
17
|
+
// Manual extraction (error-prone, repetitive)
|
|
18
|
+
val params = QueryParams("userId" -> "550e8400-e29b-41d4-a716-446655440000")
|
|
19
|
+
val userIdStr = params.getFirst("userId")
|
|
20
|
+
val userId = userIdStr match {
|
|
21
|
+
case None => Left("Missing userId")
|
|
22
|
+
case Some(s) =>
|
|
23
|
+
try Right(java.util.UUID.fromString(s))
|
|
24
|
+
catch { case e: IllegalArgumentException => Left(s"Invalid UUID format: ${e.getMessage}") }
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Every parameter requires 8+ lines of boilerplate with manual exception handling, error message creation, and type-specific parsing. UUID parsing alone involves `IllegalArgumentException` handling; multiply this across dozens of handlers extracting `UUID`, `Int`, `Boolean` parameters, and you have duplicated extraction logic everywhere — inconsistent error messages, risk of forgotten error handling, and no compile-time guarantees on correctness.
|
|
29
|
+
|
|
30
|
+
The solution is to use schema-based extraction for clean, declarative code:
|
|
31
|
+
|
|
32
|
+
```scala
|
|
33
|
+
import zio.http.QueryParams
|
|
34
|
+
import zio.http.schema._
|
|
35
|
+
|
|
36
|
+
val params = QueryParams("userId" -> "550e8400-e29b-41d4-a716-446655440000")
|
|
37
|
+
val userId = params.query[java.util.UUID]("userId") // 1 line, automatic UUID parsing + errors
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`zio-http-model-schema` separates **extraction logic from business logic**. The module achieves this through:
|
|
41
|
+
|
|
42
|
+
- **Automatic decoding** — Pass a `Schema[T]`, get `Either[Error, T]` back. Works for 11 primitive types out of the box.
|
|
43
|
+
- **Explicit error handling** — `Either` forces error handling. `QueryParamError` and `HeaderError` distinguish "missing" from "malformed" cases.
|
|
44
|
+
- **Composable** — Works directly on `QueryParams`, `Headers`, `Request`, `Response` with zero configuration.
|
|
45
|
+
- **Zero-dependency** — Pure extraction layer; doesn't pull in ZIO, async runtimes, or HTTP client libraries.
|
|
46
|
+
|
|
47
|
+
This keeps HTTP request handling clean, testable, and portable across different effect systems.
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
Add the following to your `build.sbt`:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
libraryDependencies += "dev.zio" %% "zio-http-model-schema" % "0.0.51"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
For cross-platform projects (Scala.js):
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
libraryDependencies += "dev.zio" %%% "zio-http-model-schema" % "0.0.51"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Supported Scala versions: Scala 3.x only. Requires `zio-http-model` and `zio-blocks-schema` as dependencies.
|
|
65
|
+
|
|
66
|
+
## How They Work Together
|
|
67
|
+
|
|
68
|
+
To understand how the module works, we add a **schema-based extraction layer** on top of core HTTP model types:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
HTTP Model Types (from zio-http-model)
|
|
72
|
+
├─ QueryParams: raw string key-value pairs
|
|
73
|
+
├─ Headers: raw string header name-value pairs
|
|
74
|
+
├─ Request: contains queryParams and headers
|
|
75
|
+
└─ Response: contains headers
|
|
76
|
+
|
|
77
|
+
Schema-Based Extraction (this module)
|
|
78
|
+
├─ QueryParamsSchemaOps.query[T](key) ─────┐
|
|
79
|
+
├─ HeadersSchemaOps.header[T](name) ───────┼──> StringDecoder.decode(raw, Schema[T])
|
|
80
|
+
├─ RequestSchemaOps.query[T](key) ─────────┤ ├─> Right(typedValue)
|
|
81
|
+
├─ RequestSchemaOps.header[T](name) ───────┤ └─> Left(error)
|
|
82
|
+
└─ ResponseSchemaOps.header[T](name) ──────┘
|
|
83
|
+
|
|
84
|
+
Typical Workflow:
|
|
85
|
+
1. Parse URL or receive Request (core HTTP model)
|
|
86
|
+
2. Extract queryParams or headers (access raw strings)
|
|
87
|
+
3. Use schema methods to decode to typed values (this module)
|
|
88
|
+
4. Handle Either[Error, T] in business logic
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
## Quick Showcase
|
|
93
|
+
|
|
94
|
+
Setting up and extracting query parameters with type safety:
|
|
95
|
+
|
|
96
|
+
```scala
|
|
97
|
+
import zio.http.{Request, URL}
|
|
98
|
+
import zio.http.schema._
|
|
99
|
+
|
|
100
|
+
val url = URL.parse("/api/users?page=2&limit=50&sort=name").toOption.get
|
|
101
|
+
// url: URL = URL(
|
|
102
|
+
// scheme = None,
|
|
103
|
+
// host = None,
|
|
104
|
+
// port = None,
|
|
105
|
+
// path = Path(
|
|
106
|
+
// segments = IndexedSeq("api", "users"),
|
|
107
|
+
// hasLeadingSlash = true,
|
|
108
|
+
// trailingSlash = false
|
|
109
|
+
// ),
|
|
110
|
+
// queryParams = QueryParams((page,2), (limit,50), (sort,name)),
|
|
111
|
+
// fragment = None
|
|
112
|
+
// )
|
|
113
|
+
val request = Request.get(url)
|
|
114
|
+
// request: Request = Request(
|
|
115
|
+
// method = GET,
|
|
116
|
+
// url = URL(
|
|
117
|
+
// scheme = None,
|
|
118
|
+
// host = None,
|
|
119
|
+
// port = None,
|
|
120
|
+
// path = Path(
|
|
121
|
+
// segments = IndexedSeq("api", "users"),
|
|
122
|
+
// hasLeadingSlash = true,
|
|
123
|
+
// trailingSlash = false
|
|
124
|
+
// ),
|
|
125
|
+
// queryParams = QueryParams((page,2), (limit,50), (sort,name)),
|
|
126
|
+
// fragment = None
|
|
127
|
+
// ),
|
|
128
|
+
// headers = Headers(),
|
|
129
|
+
// body = Body(length=0, contentType=ContentType(MediaType(application,octet-stream,true,true,List(bin, dms, lrf, mar, so, dist, distz, pkg, bpk, dump, elc, deploy, exe, dll, deb, dmg, iso, img, msi, msp, msm, buffer),Map(),Map()),None,None)),
|
|
130
|
+
// version = HTTP/1.1
|
|
131
|
+
// )
|
|
132
|
+
|
|
133
|
+
// Extract query parameters
|
|
134
|
+
val pageResult = request.query[Int]("page")
|
|
135
|
+
// pageResult: Either[QueryParamError, Int] = Right(2)
|
|
136
|
+
val limitResult = request.query[Int]("limit")
|
|
137
|
+
// limitResult: Either[QueryParamError, Int] = Right(50)
|
|
138
|
+
val sortResult = request.query[String]("sort")
|
|
139
|
+
// sortResult: Either[QueryParamError, String] = Right("name")
|
|
140
|
+
|
|
141
|
+
// Results are properly typed and decoded
|
|
142
|
+
(pageResult, limitResult, sortResult)
|
|
143
|
+
// res4: Tuple3[Either[QueryParamError, Int], Either[QueryParamError, Int], Either[QueryParamError, String]] = (
|
|
144
|
+
// Right(2),
|
|
145
|
+
// Right(50),
|
|
146
|
+
// Right("name")
|
|
147
|
+
// )
|
|
148
|
+
|
|
149
|
+
// Handle errors with pattern matching
|
|
150
|
+
pageResult match {
|
|
151
|
+
case Right(page) => s"Page: $page"
|
|
152
|
+
case Left(QueryParamError.Missing(key)) => s"Missing $key"
|
|
153
|
+
case Left(QueryParamError.Malformed(key, value, cause)) => s"Bad $key: $cause"
|
|
154
|
+
}
|
|
155
|
+
// res5: String = "Page: 2"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
## Extension Classes
|
|
160
|
+
|
|
161
|
+
### QueryParamsSchemaOps
|
|
162
|
+
|
|
163
|
+
Extension methods for `QueryParams` to extract and decode query parameters with type safety.
|
|
164
|
+
|
|
165
|
+
#### `QueryParams#query[T]`
|
|
166
|
+
|
|
167
|
+
Extract a single query parameter value and decode it to type `T`.
|
|
168
|
+
|
|
169
|
+
**Signature:** `query[T](key: String): Either[QueryParamError, T]`
|
|
170
|
+
|
|
171
|
+
Returns `Right(value)` if parameter exists and decoding succeeds. Returns `Left(QueryParamError.Missing(key))` if parameter is missing. Returns `Left(QueryParamError.Malformed(...))` if parameter exists but decoding fails.
|
|
172
|
+
|
|
173
|
+
When a query parameter is required, use `query[T]` and handle the error:
|
|
174
|
+
|
|
175
|
+
```scala
|
|
176
|
+
import zio.http.{URL}
|
|
177
|
+
import zio.http.schema._
|
|
178
|
+
|
|
179
|
+
val url = URL.parse("/search?q=zio").toOption.get
|
|
180
|
+
// url: URL = URL(
|
|
181
|
+
// scheme = None,
|
|
182
|
+
// host = None,
|
|
183
|
+
// port = None,
|
|
184
|
+
// path = Path(
|
|
185
|
+
// segments = IndexedSeq("search"),
|
|
186
|
+
// hasLeadingSlash = true,
|
|
187
|
+
// trailingSlash = false
|
|
188
|
+
// ),
|
|
189
|
+
// queryParams = QueryParams((q,zio)),
|
|
190
|
+
// fragment = None
|
|
191
|
+
// )
|
|
192
|
+
val params = url.queryParams
|
|
193
|
+
// params: QueryParams = QueryParams((q,zio))
|
|
194
|
+
|
|
195
|
+
params.query[String]("q") match {
|
|
196
|
+
case Right(q) => s"Search for: $q"
|
|
197
|
+
case Left(error) => s"Error: ${error.message}"
|
|
198
|
+
}
|
|
199
|
+
// res7: String = "Search for: zio"
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
#### `QueryParams#queryAll[T]`
|
|
204
|
+
|
|
205
|
+
Extract all values for a query parameter key and decode them to type `T`.
|
|
206
|
+
|
|
207
|
+
**Signature:** `queryAll[T](key: String): Either[QueryParamError, Chunk[T]]`
|
|
208
|
+
|
|
209
|
+
Returns `Right(chunk)` with all decoded values if parameter exists and all values decode successfully. Returns `Left(QueryParamError.Missing(key))` if no values exist for the key. Returns `Left(QueryParamError.Malformed(...))` if any value fails to decode.
|
|
210
|
+
|
|
211
|
+
**Pattern: Extract Multiple Values for Same Parameter**
|
|
212
|
+
|
|
213
|
+
When a query parameter appears multiple times (e.g., `?tag=scala&tag=fp`), use `queryAll[T]`:
|
|
214
|
+
|
|
215
|
+
```scala
|
|
216
|
+
import zio.http.{URL}
|
|
217
|
+
import zio.http.schema._
|
|
218
|
+
|
|
219
|
+
val url = URL.parse("/search?tag=scala&tag=functional&tag=zio").toOption.get
|
|
220
|
+
// url: URL = URL(
|
|
221
|
+
// scheme = None,
|
|
222
|
+
// host = None,
|
|
223
|
+
// port = None,
|
|
224
|
+
// path = Path(
|
|
225
|
+
// segments = IndexedSeq("search"),
|
|
226
|
+
// hasLeadingSlash = true,
|
|
227
|
+
// trailingSlash = false
|
|
228
|
+
// ),
|
|
229
|
+
// queryParams = QueryParams((tag,scala), (tag,functional), (tag,zio)),
|
|
230
|
+
// fragment = None
|
|
231
|
+
// )
|
|
232
|
+
val params = url.queryParams
|
|
233
|
+
// params: QueryParams = QueryParams((tag,scala), (tag,functional), (tag,zio))
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Extract all values for a multi-valued parameter:
|
|
237
|
+
|
|
238
|
+
```scala
|
|
239
|
+
params.queryAll[String]("tag") match {
|
|
240
|
+
case Right(tags) => s"Tags: ${tags.toList}"
|
|
241
|
+
case Left(error) => s"Error: ${error.message}"
|
|
242
|
+
}
|
|
243
|
+
// res9: String = "Tags: List(scala, functional, zio)"
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
**Short-circuit behavior:** Decoding stops at the first malformed value; only the first error is reported.
|
|
248
|
+
|
|
249
|
+
#### `QueryParams#queryOrElse[T]`
|
|
250
|
+
|
|
251
|
+
Extract a query parameter with a default fallback.
|
|
252
|
+
|
|
253
|
+
**Signature:** `queryOrElse[T](key: String, default: => T): T`
|
|
254
|
+
|
|
255
|
+
Returns the decoded value if parameter exists and decodes successfully. Returns `default` if parameter is missing or decoding fails (errors are silently ignored).
|
|
256
|
+
|
|
257
|
+
**Pattern: Extract with Default Fallback**
|
|
258
|
+
|
|
259
|
+
When a query parameter is optional with a sensible default, use `queryOrElse`:
|
|
260
|
+
|
|
261
|
+
```scala
|
|
262
|
+
import zio.http.{URL}
|
|
263
|
+
import zio.http.schema._
|
|
264
|
+
|
|
265
|
+
val url = URL.parse("/api/items?page=2").toOption.get
|
|
266
|
+
// url: URL = URL(
|
|
267
|
+
// scheme = None,
|
|
268
|
+
// host = None,
|
|
269
|
+
// port = None,
|
|
270
|
+
// path = Path(
|
|
271
|
+
// segments = IndexedSeq("api", "items"),
|
|
272
|
+
// hasLeadingSlash = true,
|
|
273
|
+
// trailingSlash = false
|
|
274
|
+
// ),
|
|
275
|
+
// queryParams = QueryParams((page,2)),
|
|
276
|
+
// fragment = None
|
|
277
|
+
// )
|
|
278
|
+
val params = url.queryParams
|
|
279
|
+
// params: QueryParams = QueryParams((page,2))
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Extract with fallback defaults:
|
|
283
|
+
|
|
284
|
+
```scala
|
|
285
|
+
val page = params.queryOrElse[Int]("page", 1)
|
|
286
|
+
// page: Int = 2
|
|
287
|
+
val limit = params.queryOrElse[Int]("limit", 20)
|
|
288
|
+
// limit: Int = 20
|
|
289
|
+
(page, limit)
|
|
290
|
+
// res11: Tuple2[Int, Int] = (2, 20)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
### HeadersSchemaOps
|
|
295
|
+
|
|
296
|
+
Extension methods for `Headers` to extract and decode header values with type safety. Uses `rawGet`/`rawGetAll` internally for raw string header access. API identical to `QueryParamsSchemaOps`.
|
|
297
|
+
|
|
298
|
+
#### `Headers#header[T]`
|
|
299
|
+
|
|
300
|
+
Extract a single header value and decode it to type `T`.
|
|
301
|
+
|
|
302
|
+
**Signature:** `header[T](name: String): Either[HeaderError, T]`
|
|
303
|
+
|
|
304
|
+
Header name matching is **case-insensitive** (HTTP spec). Returns `Right(value)` on success, `Left(HeaderError.Missing(name))` if header not found, or `Left(HeaderError.Malformed(...))` if decoding fails.
|
|
305
|
+
|
|
306
|
+
Here's how to use `header[T]`:
|
|
307
|
+
|
|
308
|
+
```scala
|
|
309
|
+
import zio.http.Headers
|
|
310
|
+
import zio.http.schema._
|
|
311
|
+
|
|
312
|
+
val headers = Headers("x-user-id" -> "42", "x-api-version" -> "2")
|
|
313
|
+
// headers: Headers = Headers(x-user-id: 42, x-api-version: 2)
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Calling `header[T]` returns an `Either` with the decoded value or an error:
|
|
317
|
+
|
|
318
|
+
```scala
|
|
319
|
+
headers.header[Int]("x-user-id")
|
|
320
|
+
// res13: Either[HeaderError, Int] = Right(42)
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Header names are **case-insensitive**:
|
|
324
|
+
|
|
325
|
+
```scala
|
|
326
|
+
headers.header[Int]("X-User-ID")
|
|
327
|
+
// res14: Either[HeaderError, Int] = Right(42)
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Missing headers produce a `Missing` error:
|
|
331
|
+
|
|
332
|
+
```scala
|
|
333
|
+
headers.header[Int]("x-missing")
|
|
334
|
+
// res15: Either[HeaderError, Int] = Left(Missing("x-missing"))
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
You can also decode with a custom `Header.Codec[A]` when the header type belongs to your domain instead of the core HTTP model:
|
|
338
|
+
|
|
339
|
+
```scala
|
|
340
|
+
import zio.http.{Header, Headers}
|
|
341
|
+
import zio.http.schema._
|
|
342
|
+
|
|
343
|
+
object TraceIdHeader extends Header.Codec[String] {
|
|
344
|
+
def name: String = "x-trace-id"
|
|
345
|
+
def parse(value: String): Either[String, String] =
|
|
346
|
+
if (value.startsWith("trace-")) Right(value) else Left("trace id must start with trace-")
|
|
347
|
+
def render(value: String): String = value
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
val customHeaders = Headers("x-trace-id" -> "trace-123")
|
|
351
|
+
// customHeaders: Headers = Headers(x-trace-id: trace-123)
|
|
352
|
+
customHeaders.header(TraceIdHeader)
|
|
353
|
+
// res16: Either[HeaderError, String] = Right("trace-123")
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
#### `Headers#headerAll[T]`
|
|
358
|
+
|
|
359
|
+
Extract all values for a header name and decode them to type `T`.
|
|
360
|
+
|
|
361
|
+
**Signature:** `headerAll[T](name: String): Either[HeaderError, Chunk[T]]`
|
|
362
|
+
|
|
363
|
+
HTTP allows multiple headers with the same name; this method collects and decodes all of them. Returns `Right(chunk)` with all decoded values, `Left(HeaderError.Missing(name))` if no headers exist for the name, or `Left(HeaderError.Malformed(...))` if any value fails to decode.
|
|
364
|
+
|
|
365
|
+
Here's how to extract multiple headers:
|
|
366
|
+
|
|
367
|
+
```scala
|
|
368
|
+
import zio.http.Headers
|
|
369
|
+
import zio.http.schema._
|
|
370
|
+
|
|
371
|
+
val headers = Headers("x-tag" -> "scala", "x-tag" -> "functional", "x-tag" -> "zio")
|
|
372
|
+
// headers: Headers = Headers(x-tag: scala, x-tag: functional, x-tag: zio)
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Extract all values for a header:
|
|
376
|
+
|
|
377
|
+
```scala
|
|
378
|
+
headers.headerAll[String]("x-tag")
|
|
379
|
+
// res18: Either[HeaderError, Chunk[String]] = Right(
|
|
380
|
+
// IndexedSeq("scala", "functional", "zio")
|
|
381
|
+
// )
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Missing headers return a `Missing` error:
|
|
385
|
+
|
|
386
|
+
```scala
|
|
387
|
+
headers.headerAll[String]("x-missing")
|
|
388
|
+
// res19: Either[HeaderError, Chunk[String]] = Left(Missing("x-missing"))
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
The codec-based overload works for repeated headers as well and reports the first malformed value as `HeaderError.Malformed`:
|
|
392
|
+
|
|
393
|
+
```scala
|
|
394
|
+
import zio.http.{Header, Headers}
|
|
395
|
+
import zio.http.schema._
|
|
396
|
+
|
|
397
|
+
object TraceIdHeader extends Header.Codec[String] {
|
|
398
|
+
def name: String = "x-trace-id"
|
|
399
|
+
def parse(value: String): Either[String, String] =
|
|
400
|
+
if (value.startsWith("trace-")) Right(value) else Left("trace id must start with trace-")
|
|
401
|
+
def render(value: String): String = value
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
val repeatedHeaders = Headers("x-trace-id" -> "trace-1", "x-trace-id" -> "trace-2")
|
|
405
|
+
// repeatedHeaders: Headers = Headers(x-trace-id: trace-1, x-trace-id: trace-2)
|
|
406
|
+
repeatedHeaders.headerAll(TraceIdHeader)
|
|
407
|
+
// res20: Either[HeaderError, Chunk[String]] = Right(
|
|
408
|
+
// IndexedSeq("trace-1", "trace-2")
|
|
409
|
+
// )
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
#### `Headers#headerOrElse[T]`
|
|
414
|
+
|
|
415
|
+
Extract a header with a default fallback (errors are silently ignored).
|
|
416
|
+
|
|
417
|
+
**Signature:** `headerOrElse[T](name: String, default: => T): T`
|
|
418
|
+
|
|
419
|
+
Use `headerOrElse[T]` when a header is optional with a sensible default:
|
|
420
|
+
|
|
421
|
+
```scala
|
|
422
|
+
import zio.http.Headers
|
|
423
|
+
import zio.http.schema._
|
|
424
|
+
|
|
425
|
+
val headers = Headers("x-count" -> "5")
|
|
426
|
+
// headers: Headers = Headers(x-count: 5)
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
When the header exists, it's decoded and returned:
|
|
430
|
+
|
|
431
|
+
```scala
|
|
432
|
+
headers.headerOrElse[Int]("x-count", 0)
|
|
433
|
+
// res22: Int = 5
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
When missing, the default is used:
|
|
437
|
+
|
|
438
|
+
```scala
|
|
439
|
+
headers.headerOrElse[Int]("x-missing", 0)
|
|
440
|
+
// res23: Int = 0
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
### RequestSchemaOps
|
|
445
|
+
|
|
446
|
+
Extension methods for `Request` to extract query parameters and headers using the same schema-based API.
|
|
447
|
+
|
|
448
|
+
Exposes all methods from `QueryParamsSchemaOps` and `HeadersSchemaOps` directly on `Request` — they work identically but operate on the request object.
|
|
449
|
+
|
|
450
|
+
Query parameters and headers are extracted identically; just use `header[T]` or `headerAll[T]`:
|
|
451
|
+
|
|
452
|
+
```scala
|
|
453
|
+
import zio.http.{Request, URL}
|
|
454
|
+
import zio.http.schema._
|
|
455
|
+
|
|
456
|
+
val request = Request.get(URL.parse("/").toOption.get)
|
|
457
|
+
.addHeader("x-user-id", "42")
|
|
458
|
+
.addHeader("x-api-version", "2")
|
|
459
|
+
// request: Request = Request(
|
|
460
|
+
// method = GET,
|
|
461
|
+
// url = URL(
|
|
462
|
+
// scheme = None,
|
|
463
|
+
// host = None,
|
|
464
|
+
// port = None,
|
|
465
|
+
// path = Path(
|
|
466
|
+
// segments = IndexedSeq(),
|
|
467
|
+
// hasLeadingSlash = true,
|
|
468
|
+
// trailingSlash = false
|
|
469
|
+
// ),
|
|
470
|
+
// queryParams = QueryParams(),
|
|
471
|
+
// fragment = None
|
|
472
|
+
// ),
|
|
473
|
+
// headers = Headers(x-user-id: 42, x-api-version: 2),
|
|
474
|
+
// body = Body(length=0, contentType=ContentType(MediaType(application,octet-stream,true,true,List(bin, dms, lrf, mar, so, dist, distz, pkg, bpk, dump, elc, deploy, exe, dll, deb, dmg, iso, img, msi, msp, msm, buffer),Map(),Map()),None,None)),
|
|
475
|
+
// version = HTTP/1.1
|
|
476
|
+
// )
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
Extract headers from the request using the headers API:
|
|
480
|
+
|
|
481
|
+
```scala
|
|
482
|
+
val userId = request.headers.header[Int]("x-user-id")
|
|
483
|
+
// userId: Either[HeaderError, Int] = Right(42)
|
|
484
|
+
val apiVersion = request.headers.headerOrElse[Int]("x-api-version", 1)
|
|
485
|
+
// apiVersion: Int = 2
|
|
486
|
+
(userId, apiVersion)
|
|
487
|
+
// res25: Tuple2[Either[HeaderError, Int], Int] = (Right(42), 2)
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
### ResponseSchemaOps
|
|
492
|
+
|
|
493
|
+
Extension methods for `Response` to extract headers using the schema-based API.
|
|
494
|
+
|
|
495
|
+
Exposes all header methods from `HeadersSchemaOps` directly on `Response` — they work identically but operate on the response object. Note: `Response` does not have `query*` methods (responses don't have query parameters).
|
|
496
|
+
|
|
497
|
+
`Response` provides the same header extraction methods:
|
|
498
|
+
|
|
499
|
+
```scala
|
|
500
|
+
import zio.http.Response
|
|
501
|
+
import zio.http.schema._
|
|
502
|
+
|
|
503
|
+
val response = Response.ok
|
|
504
|
+
.addHeader("x-request-id", "req-12345")
|
|
505
|
+
.addHeader("x-ratelimit-remaining", "99")
|
|
506
|
+
// response: Response = Response(
|
|
507
|
+
// status = 200,
|
|
508
|
+
// headers = Headers(x-request-id: req-12345, x-ratelimit-remaining: 99),
|
|
509
|
+
// body = Body(length=0, contentType=ContentType(MediaType(application,octet-stream,true,true,List(bin, dms, lrf, mar, so, dist, distz, pkg, bpk, dump, elc, deploy, exe, dll, deb, dmg, iso, img, msi, msp, msm, buffer),Map(),Map()),None,None)),
|
|
510
|
+
// version = HTTP/1.1
|
|
511
|
+
// )
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
Extract a header from the response using the headers API:
|
|
515
|
+
|
|
516
|
+
```scala
|
|
517
|
+
response.headers.header[String]("x-request-id")
|
|
518
|
+
// res27: Either[HeaderError, String] = Right("req-12345")
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
Use a default if the header is missing:
|
|
522
|
+
|
|
523
|
+
```scala
|
|
524
|
+
response.headers.headerOrElse[Int]("x-ratelimit-remaining", 100)
|
|
525
|
+
// res28: Int = 99
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
## Composing Multiple Extractions
|
|
530
|
+
|
|
531
|
+
Extract multiple parameters or headers in a single operation using `Either`'s monadic operations:
|
|
532
|
+
|
|
533
|
+
```scala
|
|
534
|
+
import zio.http.{Request, URL}
|
|
535
|
+
import zio.http.schema._
|
|
536
|
+
|
|
537
|
+
val request = Request.get(URL.parse("/api/posts?userId=5&page=2").toOption.get)
|
|
538
|
+
// request: Request = Request(
|
|
539
|
+
// method = GET,
|
|
540
|
+
// url = URL(
|
|
541
|
+
// scheme = None,
|
|
542
|
+
// host = None,
|
|
543
|
+
// port = None,
|
|
544
|
+
// path = Path(
|
|
545
|
+
// segments = IndexedSeq("api", "posts"),
|
|
546
|
+
// hasLeadingSlash = true,
|
|
547
|
+
// trailingSlash = false
|
|
548
|
+
// ),
|
|
549
|
+
// queryParams = QueryParams((userId,5), (page,2)),
|
|
550
|
+
// fragment = None
|
|
551
|
+
// ),
|
|
552
|
+
// headers = Headers(),
|
|
553
|
+
// body = Body(length=0, contentType=ContentType(MediaType(application,octet-stream,true,true,List(bin, dms, lrf, mar, so, dist, distz, pkg, bpk, dump, elc, deploy, exe, dll, deb, dmg, iso, img, msi, msp, msm, buffer),Map(),Map()),None,None)),
|
|
554
|
+
// version = HTTP/1.1
|
|
555
|
+
// )
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
Combine multiple extractions with a for-comprehension:
|
|
559
|
+
|
|
560
|
+
```scala
|
|
561
|
+
val result = for {
|
|
562
|
+
userId <- request.query[Int]("userId")
|
|
563
|
+
page <- request.query[Int]("page")
|
|
564
|
+
} yield (userId, page)
|
|
565
|
+
// result: Either[QueryParamError, Tuple2[Int, Int]] = Right((5, 2))
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
Handle the combined result:
|
|
569
|
+
|
|
570
|
+
```scala
|
|
571
|
+
result match {
|
|
572
|
+
case Right((userId, page)) => s"User $userId, page $page"
|
|
573
|
+
case Left(error) => s"Extraction failed: ${error.message}"
|
|
574
|
+
}
|
|
575
|
+
// res30: String = "User 5, page 2"
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
The for-comprehension short-circuits on the first error, so only the first error is reported if any extraction fails. This pattern is useful when you need multiple parameters to be present and valid before proceeding with business logic.
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
## Error Handling
|
|
582
|
+
|
|
583
|
+
The module provides two error types for explicit error handling: `QueryParamError` and `HeaderError`.
|
|
584
|
+
|
|
585
|
+
### QueryParamError
|
|
586
|
+
|
|
587
|
+
Error type for query parameter extraction failures:
|
|
588
|
+
|
|
589
|
+
```scala
|
|
590
|
+
sealed trait QueryParamError extends Product with Serializable {
|
|
591
|
+
def message: String
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
object QueryParamError {
|
|
595
|
+
final case class Missing(key: String) extends QueryParamError {
|
|
596
|
+
def message: String = s"Missing query parameter: $key"
|
|
597
|
+
}
|
|
598
|
+
final case class Malformed(key: String, value: String, cause: String) extends QueryParamError {
|
|
599
|
+
def message: String = s"Malformed query parameter '$key' value '$value': $cause"
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
**Variants:**
|
|
605
|
+
|
|
606
|
+
- **`Missing(key)`** — Query parameter with name `key` is not present in the parameters
|
|
607
|
+
- Example: `QueryParamError.Missing("page")` when accessing a non-existent parameter
|
|
608
|
+
- Message: `"Missing query parameter: page"`
|
|
609
|
+
|
|
610
|
+
- **`Malformed(key, value, cause)`** — Query parameter with name `key` is present but decoding the `value` to the requested type fails
|
|
611
|
+
- Example: `QueryParamError.Malformed("age", "abc", "Cannot parse 'abc' as Int")` when `age=abc` but `Int` was requested
|
|
612
|
+
- Message: `"Malformed query parameter 'age' value 'abc': Cannot parse 'abc' as Int"`
|
|
613
|
+
|
|
614
|
+
**Accessing error messages:**
|
|
615
|
+
|
|
616
|
+
All `QueryParamError` subtypes have a `message` property for user-friendly error reporting:
|
|
617
|
+
|
|
618
|
+
```scala
|
|
619
|
+
import zio.http.schema._
|
|
620
|
+
|
|
621
|
+
val error: QueryParamError = QueryParamError.Malformed("page", "invalid", "Cannot parse 'invalid' as Int")
|
|
622
|
+
// error: QueryParamError = Malformed(
|
|
623
|
+
// key = "page",
|
|
624
|
+
// value = "invalid",
|
|
625
|
+
// cause = "Cannot parse 'invalid' as Int"
|
|
626
|
+
// )
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
The message provides detailed error information:
|
|
630
|
+
|
|
631
|
+
```scala
|
|
632
|
+
error.message
|
|
633
|
+
// res33: String = "Malformed query parameter 'page' value 'invalid': Cannot parse 'invalid' as Int"
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
### HeaderError
|
|
637
|
+
|
|
638
|
+
Error type for header extraction failures. Structurally identical to `QueryParamError`, with `name` replacing `key` and header-specific message prefixes:
|
|
639
|
+
|
|
640
|
+
```scala
|
|
641
|
+
sealed trait HeaderError extends Product with Serializable {
|
|
642
|
+
def message: String
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
object HeaderError {
|
|
646
|
+
final case class Missing(name: String) extends HeaderError {
|
|
647
|
+
def message: String = s"Missing header: $name"
|
|
648
|
+
}
|
|
649
|
+
final case class Malformed(name: String, value: String, cause: String) extends HeaderError {
|
|
650
|
+
def message: String = s"Malformed header '$name' value '$value': $cause"
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
**Handling patterns:**
|
|
656
|
+
|
|
657
|
+
Pattern-match on error type to distinguish "missing" from "malformed":
|
|
658
|
+
|
|
659
|
+
```scala
|
|
660
|
+
import zio.http.{Request, URL}
|
|
661
|
+
import zio.http.schema._
|
|
662
|
+
|
|
663
|
+
val request = Request.get(URL.parse("/").toOption.get)
|
|
664
|
+
.addHeader("x-token", "invalid-token")
|
|
665
|
+
// request: Request = Request(
|
|
666
|
+
// method = GET,
|
|
667
|
+
// url = URL(
|
|
668
|
+
// scheme = None,
|
|
669
|
+
// host = None,
|
|
670
|
+
// port = None,
|
|
671
|
+
// path = Path(
|
|
672
|
+
// segments = IndexedSeq(),
|
|
673
|
+
// hasLeadingSlash = true,
|
|
674
|
+
// trailingSlash = false
|
|
675
|
+
// ),
|
|
676
|
+
// queryParams = QueryParams(),
|
|
677
|
+
// fragment = None
|
|
678
|
+
// ),
|
|
679
|
+
// headers = Headers(x-token: invalid-token),
|
|
680
|
+
// body = Body(length=0, contentType=ContentType(MediaType(application,octet-stream,true,true,List(bin, dms, lrf, mar, so, dist, distz, pkg, bpk, dump, elc, deploy, exe, dll, deb, dmg, iso, img, msi, msp, msm, buffer),Map(),Map()),None,None)),
|
|
681
|
+
// version = HTTP/1.1
|
|
682
|
+
// )
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
When you extract a header with the wrong type, you get a `Malformed` error:
|
|
686
|
+
|
|
687
|
+
```scala
|
|
688
|
+
request.headers.header[Int]("x-token") match {
|
|
689
|
+
case Right(token) => s"Token: $token"
|
|
690
|
+
case Left(HeaderError.Missing(name)) => s"Missing required header: $name"
|
|
691
|
+
case Left(HeaderError.Malformed(name, value, cause)) => s"Bad header: $cause"
|
|
692
|
+
}
|
|
693
|
+
// res35: String = "Bad header: Cannot parse 'invalid-token' as Int"
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
## Supported Types
|
|
697
|
+
|
|
698
|
+
The module supports decoding to any type with a `Schema[T]` instance. Built-in support includes:
|
|
699
|
+
|
|
700
|
+
### Primitives
|
|
701
|
+
|
|
702
|
+
- **`String`** — No decoding, raw string value
|
|
703
|
+
- **`Int`** — Parsed via `String#toInt`, error on invalid format
|
|
704
|
+
- **`Long`** — Parsed via `String#toLong`, error on invalid format
|
|
705
|
+
- **`Boolean`** — Parsed via `String#toBoolean` (case-insensitive; accepts "true"/"True"/"TRUE" → true and "false"/"False"/"FALSE" → false; any other value produces a Malformed error)
|
|
706
|
+
- **`Double`** — Parsed via `String#toDouble`, error on invalid format
|
|
707
|
+
- **`Float`** — Parsed via `String#toFloat`, error on invalid format
|
|
708
|
+
- **`Short`** — Parsed via `String#toShort`, error on invalid format
|
|
709
|
+
- **`Byte`** — Parsed via `String#toByte`, error on invalid format
|
|
710
|
+
- **`Char`** — Parses single character; returns a `Left` with error message `"Expected single character but got 'value'"` if string length ≠ 1 (differs from standard error pattern)
|
|
711
|
+
|
|
712
|
+
### Big Numbers
|
|
713
|
+
|
|
714
|
+
- **`BigInt`** — Parsed via `scala.BigInt(string)`, error on invalid format
|
|
715
|
+
- **`BigDecimal`** — Parsed via `scala.BigDecimal(string)`, error on invalid format
|
|
716
|
+
|
|
717
|
+
### UUID
|
|
718
|
+
|
|
719
|
+
- **`java.util.UUID`** — Parsed via `java.util.UUID.fromString(string)`, error on invalid format (must be standard UUID format: `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`)
|
|
720
|
+
|
|
721
|
+
### Error Messages
|
|
722
|
+
|
|
723
|
+
Most decoding errors follow the pattern: `"Cannot parse 'value' as TypeName"`. Example error messages:
|
|
724
|
+
|
|
725
|
+
Here are common error message formats:
|
|
726
|
+
|
|
727
|
+
```
|
|
728
|
+
Cannot parse 'abc' as Int
|
|
729
|
+
Cannot parse 'notaboolean' as Boolean
|
|
730
|
+
Cannot parse 'not-a-uuid' as UUID
|
|
731
|
+
Cannot parse '12.34.56' as BigDecimal
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
**Exception:** `Char` parsing uses a different error message format:
|
|
735
|
+
|
|
736
|
+
```
|
|
737
|
+
Expected single character but got 'multichar'
|
|
738
|
+
Expected single character but got ''
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
### Custom Types
|
|
742
|
+
|
|
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, manually create a `Schema[T]` using the schema module's derivation tools or manual construction.
|
|
744
|
+
|
|
745
|
+
## See Also
|
|
746
|
+
|
|
747
|
+
- [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.
|