@zio.dev/zio-blocks 0.0.25 → 0.0.27
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/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 +1 -1
- package/guides/zio-schema-migration.md +1195 -0
- package/index.md +14 -11
- package/package.json +1 -1
- package/reference/allows.md +352 -0
- package/reference/codec.md +10 -10
- package/reference/docs.md +1 -1
- package/reference/media-type.md +2 -2
- package/reference/modifier.md +9 -9
- package/reference/schema-error.md +569 -0
- package/reference/schema-expr.md +2 -2
- package/reference/schema.md +29 -0
- package/reference/type-class-derivation.md +329 -324
- package/reference/validation.md +1 -1
- package/reference/xml.md +743 -0
- package/scope.md +150 -8
- package/sidebars.js +2 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: schema-error
|
|
3
|
+
title: "SchemaError"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`SchemaError` is a **structured error type** for schema operations in ZIO Blocks. It represents one or more validation, conversion, or structural failures that occurred while decoding, encoding, or transforming data, each annotated with a [`DynamicOptic`](./dynamic-optic.md) path that pinpoints the failing location in the data structure.
|
|
7
|
+
|
|
8
|
+
```scala
|
|
9
|
+
final case class SchemaError(errors: ::[SchemaError.Single])
|
|
10
|
+
extends Exception with NoStackTrace
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Here is the full structure of `SchemaError`:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
SchemaError
|
|
17
|
+
└── errors: ::[Single] (non-empty list — always at least one failure)
|
|
18
|
+
│
|
|
19
|
+
└── Single (sealed trait)
|
|
20
|
+
│ ├── source: DynamicOptic (path to the failing location)
|
|
21
|
+
│ └── message: String (human-readable description)
|
|
22
|
+
│
|
|
23
|
+
├── ConversionFailed (type or value conversion failed)
|
|
24
|
+
├── MissingField (required field absent)
|
|
25
|
+
├── DuplicatedField (same field key appears more than once)
|
|
26
|
+
├── ExpectationMismatch (wrong DynamicValue variant encountered)
|
|
27
|
+
├── UnknownCase (unrecognised sealed-trait discriminator)
|
|
28
|
+
└── Message (free-form message, optional path)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
`SchemaError`:
|
|
33
|
+
|
|
34
|
+
- Aggregates multiple independent failures into a single error value
|
|
35
|
+
- Annotates every failure with a precise traversal path through the data
|
|
36
|
+
- Extends `Exception` so it can be thrown and caught with standard JVM mechanisms
|
|
37
|
+
- Suppresses stack traces via `NoStackTrace` — error location is conveyed through the path, not the JVM stack
|
|
38
|
+
|
|
39
|
+
## Motivation
|
|
40
|
+
|
|
41
|
+
When decoding a complex nested value, a single structural problem — a missing field, a type mismatch, an unknown case discriminator — must be reported together with the location where it occurred. In a large schema, multiple independent problems can coexist, and surfacing them all at once saves the caller round-trips.
|
|
42
|
+
|
|
43
|
+
Every `Schema#fromDynamicValue`, every `Codec#decode`, and every optic traversal that can fail returns `Either[SchemaError, A]`. The same type carries both structural errors (missing fields, wrong types) and domain validation errors (value out of range, blank string), so callers deal with a single error channel.
|
|
44
|
+
|
|
45
|
+
`SchemaError` was introduced to replace the earlier `JsonError` and `DynamicValueError` types that existed as separate error channels for each format. Those types used string concatenation (`"error1; error2"`) to combine failures via `++`, which silently discarded path information from the second error onward. `SchemaError` solves this by maintaining a non-empty list (`::`) of `Single` failures — each one independently annotated with its own `DynamicOptic` path — so no information is lost during aggregation.
|
|
46
|
+
|
|
47
|
+
Here is a quick taste of how `SchemaError` behaves:
|
|
48
|
+
|
|
49
|
+
```scala
|
|
50
|
+
import zio.blocks.schema.SchemaError
|
|
51
|
+
|
|
52
|
+
// Create a simple message error
|
|
53
|
+
val err = SchemaError("Age must be positive")
|
|
54
|
+
|
|
55
|
+
// Annotate with the location in the data
|
|
56
|
+
val located = SchemaError.missingField(Nil, "email").atField("user")
|
|
57
|
+
println(located.message) // Missing field 'email' at: .user
|
|
58
|
+
|
|
59
|
+
// Combine two independent failures
|
|
60
|
+
val combined = SchemaError("name is blank") ++ SchemaError("age is negative")
|
|
61
|
+
println(combined.errors.length) // 2
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Construction / Creating Instances
|
|
65
|
+
|
|
66
|
+
### `SchemaError.apply`
|
|
67
|
+
|
|
68
|
+
The simplest constructor — creates a free-form `Message` error at the root path:
|
|
69
|
+
|
|
70
|
+
```scala
|
|
71
|
+
object SchemaError {
|
|
72
|
+
def apply(details: String): SchemaError
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```scala
|
|
77
|
+
import zio.blocks.schema.SchemaError
|
|
78
|
+
|
|
79
|
+
val err = SchemaError("Value must be positive")
|
|
80
|
+
println(err.message) // Value must be positive
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### `SchemaError.message`
|
|
84
|
+
|
|
85
|
+
Creates a `Message` error with a free-form description and an optional `DynamicOptic` path. When no path is supplied it defaults to the root.
|
|
86
|
+
|
|
87
|
+
```scala
|
|
88
|
+
object SchemaError {
|
|
89
|
+
def message(details: String, path: DynamicOptic = DynamicOptic.root): SchemaError
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Here we create a message error at the root, and another with an explicit path:
|
|
94
|
+
|
|
95
|
+
```scala
|
|
96
|
+
import zio.blocks.schema.{DynamicOptic, SchemaError}
|
|
97
|
+
|
|
98
|
+
// Root-level message (same as SchemaError.apply)
|
|
99
|
+
val atRoot = SchemaError.message("Unexpected null")
|
|
100
|
+
println(atRoot.message) // Unexpected null
|
|
101
|
+
|
|
102
|
+
// Message with an explicit path
|
|
103
|
+
val path = DynamicOptic.root.field("address")
|
|
104
|
+
val atPath = SchemaError.message("Unexpected null", path)
|
|
105
|
+
println(atPath.message) // Unexpected null at: .address
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### `SchemaError.validationFailed`
|
|
109
|
+
|
|
110
|
+
Convenience factory for validation failures. Equivalent to `SchemaError.conversionFailed(Nil, message)`, designed for smart constructors that return string-based error messages.
|
|
111
|
+
|
|
112
|
+
```scala
|
|
113
|
+
object SchemaError {
|
|
114
|
+
def validationFailed(message: String): SchemaError
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Here is an example:
|
|
119
|
+
|
|
120
|
+
```scala
|
|
121
|
+
import zio.blocks.schema.SchemaError
|
|
122
|
+
|
|
123
|
+
val err = SchemaError.validationFailed("Age must be between 0 and 150")
|
|
124
|
+
println(err.message) // Age must be between 0 and 150
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### `SchemaError.conversionFailed`
|
|
128
|
+
|
|
129
|
+
Creates a `ConversionFailed` error for a failed type or value conversion. Two overloads exist.
|
|
130
|
+
|
|
131
|
+
```scala
|
|
132
|
+
object SchemaError {
|
|
133
|
+
def conversionFailed(trace: List[DynamicOptic.Node], details: String): SchemaError
|
|
134
|
+
def conversionFailed(contextMessage: String, cause: SchemaError): SchemaError
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The first overload is used by codecs — `trace` is the list of path nodes accumulated during decoding. Pass `Nil` when constructing an error manually and use the `at*` methods to set the path. The second overload wraps a nested `SchemaError` with additional context; the nested failures are rendered under a "Caused by:" section.
|
|
139
|
+
|
|
140
|
+
```scala
|
|
141
|
+
import zio.blocks.schema.SchemaError
|
|
142
|
+
|
|
143
|
+
// Root-level conversion failure
|
|
144
|
+
val err = SchemaError.conversionFailed(Nil, "Expected a positive integer")
|
|
145
|
+
println(err.message) // Expected a positive integer
|
|
146
|
+
|
|
147
|
+
// Wrapping a nested failure with context
|
|
148
|
+
val inner = SchemaError("name must not be empty") ++
|
|
149
|
+
SchemaError("age must be positive")
|
|
150
|
+
val outer = SchemaError.conversionFailed("Person construction failed", inner)
|
|
151
|
+
println(outer.message)
|
|
152
|
+
// Person construction failed
|
|
153
|
+
// Caused by:
|
|
154
|
+
// - name must not be empty
|
|
155
|
+
// - age must be positive
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### `SchemaError.missingField`
|
|
159
|
+
|
|
160
|
+
Creates a `MissingField` error indicating that a required field was absent from the decoded representation.
|
|
161
|
+
|
|
162
|
+
```scala
|
|
163
|
+
object SchemaError {
|
|
164
|
+
def missingField(trace: List[DynamicOptic.Node], fieldName: String): SchemaError
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
```scala
|
|
169
|
+
import zio.blocks.schema.SchemaError
|
|
170
|
+
|
|
171
|
+
val err = SchemaError.missingField(Nil, "email").atField("user")
|
|
172
|
+
println(err.message) // Missing field 'email' at: .user
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### `SchemaError.duplicatedField`
|
|
176
|
+
|
|
177
|
+
Creates a `DuplicatedField` error indicating that the same field key appeared more than once in the encoded form.
|
|
178
|
+
|
|
179
|
+
```scala
|
|
180
|
+
object SchemaError {
|
|
181
|
+
def duplicatedField(trace: List[DynamicOptic.Node], fieldName: String): SchemaError
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
```scala
|
|
186
|
+
import zio.blocks.schema.SchemaError
|
|
187
|
+
|
|
188
|
+
val err = SchemaError.duplicatedField(Nil, "id").atField("record")
|
|
189
|
+
println(err.message) // Duplicated field 'id' at: .record
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### `SchemaError.expectationMismatch`
|
|
193
|
+
|
|
194
|
+
Creates an `ExpectationMismatch` error indicating that the encountered `DynamicValue` variant does not match what the schema expected.
|
|
195
|
+
|
|
196
|
+
```scala
|
|
197
|
+
object SchemaError {
|
|
198
|
+
def expectationMismatch(trace: List[DynamicOptic.Node], expectation: String): SchemaError
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
```scala
|
|
203
|
+
import zio.blocks.schema.SchemaError
|
|
204
|
+
|
|
205
|
+
val err = SchemaError
|
|
206
|
+
.expectationMismatch(Nil, "Expected Record, got Sequence")
|
|
207
|
+
.atField("data")
|
|
208
|
+
println(err.message) // Expected Record, got Sequence at: .data
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### `SchemaError.unknownCase`
|
|
212
|
+
|
|
213
|
+
Creates an `UnknownCase` error indicating that the decoded discriminator value does not correspond to any known variant of a sealed trait.
|
|
214
|
+
|
|
215
|
+
```scala
|
|
216
|
+
object SchemaError {
|
|
217
|
+
def unknownCase(trace: List[DynamicOptic.Node], caseName: String): SchemaError
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
```scala
|
|
222
|
+
import zio.blocks.schema.SchemaError
|
|
223
|
+
|
|
224
|
+
val err = SchemaError.unknownCase(Nil, "Triangle").atField("shape")
|
|
225
|
+
println(err.message) // Unknown case 'Triangle' at: .shape
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Core Operations
|
|
229
|
+
|
|
230
|
+
### Error Messages
|
|
231
|
+
|
|
232
|
+
#### `message`
|
|
233
|
+
|
|
234
|
+
Returns all individual error messages joined with newlines.
|
|
235
|
+
|
|
236
|
+
```scala
|
|
237
|
+
final case class SchemaError(errors: ::[SchemaError.Single]) {
|
|
238
|
+
def message: String
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
```scala
|
|
243
|
+
import zio.blocks.schema.SchemaError
|
|
244
|
+
|
|
245
|
+
val err = SchemaError("first failure") ++ SchemaError("second failure")
|
|
246
|
+
println(err.message)
|
|
247
|
+
// first failure
|
|
248
|
+
// second failure
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
#### `getMessage`
|
|
252
|
+
|
|
253
|
+
Delegates to `message`. Because `SchemaError` extends `Exception`, `getMessage` is called by the JVM when the exception is printed or logged by frameworks.
|
|
254
|
+
|
|
255
|
+
```scala
|
|
256
|
+
final case class SchemaError(errors: ::[SchemaError.Single]) {
|
|
257
|
+
def getMessage: String
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
```scala
|
|
262
|
+
import zio.blocks.schema.SchemaError
|
|
263
|
+
|
|
264
|
+
val err = SchemaError("something went wrong")
|
|
265
|
+
assert(err.getMessage == err.message)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Error Aggregation
|
|
269
|
+
|
|
270
|
+
#### `++`
|
|
271
|
+
|
|
272
|
+
Combines two `SchemaError` values into one, preserving all individual `Single` failures from both sides. We use `++` to accumulate errors from independent parts of a schema — for example, multiple record fields that are each decoded independently.
|
|
273
|
+
|
|
274
|
+
```scala
|
|
275
|
+
final case class SchemaError(errors: ::[SchemaError.Single]) {
|
|
276
|
+
def ++(other: SchemaError): SchemaError
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
```scala
|
|
281
|
+
import zio.blocks.schema.SchemaError
|
|
282
|
+
|
|
283
|
+
val nameError = SchemaError.missingField(Nil, "name")
|
|
284
|
+
val ageError = SchemaError.conversionFailed(Nil, "Age must be positive")
|
|
285
|
+
val combined = nameError ++ ageError
|
|
286
|
+
|
|
287
|
+
println(combined.errors.length) // 2
|
|
288
|
+
println(combined.message)
|
|
289
|
+
// Missing field 'name' at: .
|
|
290
|
+
// Age must be positive
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
`++` is associative: `(a ++ b) ++ c` and `a ++ (b ++ c)` produce the same set of errors.
|
|
294
|
+
|
|
295
|
+
### Path Annotation
|
|
296
|
+
|
|
297
|
+
Path annotation methods prepend a path segment to the `source` of every `SchemaError.Single` inside the error. Codecs call these methods as they unwind the call stack — the innermost call adds the innermost path segment, and the outermost call adds the outermost one.
|
|
298
|
+
|
|
299
|
+
#### `atField`
|
|
300
|
+
|
|
301
|
+
Prepends a record field access to the path of all errors.
|
|
302
|
+
|
|
303
|
+
```scala
|
|
304
|
+
final case class SchemaError(errors: ::[SchemaError.Single]) {
|
|
305
|
+
def atField(name: String): SchemaError
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
```scala
|
|
310
|
+
import zio.blocks.schema.SchemaError
|
|
311
|
+
|
|
312
|
+
// Codec decoding 'city' inside 'address' inside 'user'
|
|
313
|
+
val err = SchemaError.missingField(Nil, "city")
|
|
314
|
+
.atField("address") // called by the address codec
|
|
315
|
+
.atField("user") // called by the user codec
|
|
316
|
+
println(err.message) // Missing field 'city' at: .user.address
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
#### `atIndex`
|
|
320
|
+
|
|
321
|
+
Prepends a sequence index access to the path of all errors.
|
|
322
|
+
|
|
323
|
+
```scala
|
|
324
|
+
final case class SchemaError(errors: ::[SchemaError.Single]) {
|
|
325
|
+
def atIndex(index: Int): SchemaError
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
```scala
|
|
330
|
+
import zio.blocks.schema.SchemaError
|
|
331
|
+
|
|
332
|
+
val err = SchemaError("invalid phone number").atIndex(2).atField("phones")
|
|
333
|
+
println(err.message) // invalid phone number at: .phones[2]
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
#### `atCase`
|
|
337
|
+
|
|
338
|
+
Prepends a sealed-trait case access to the path of all errors. In the compact path notation used by `Message`, the case name appears as `<CaseName>`.
|
|
339
|
+
|
|
340
|
+
```scala
|
|
341
|
+
final case class SchemaError(errors: ::[SchemaError.Single]) {
|
|
342
|
+
def atCase(name: String): SchemaError
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
```scala
|
|
347
|
+
import zio.blocks.schema.SchemaError
|
|
348
|
+
|
|
349
|
+
val err = SchemaError("conversion failed").atField("value").atCase("Right")
|
|
350
|
+
println(err.message) // conversion failed at: <Right>.value
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
#### `atKey`
|
|
354
|
+
|
|
355
|
+
Prepends a map key access to the path of all errors. The key is a [`DynamicValue`](./dynamic-value.md), rendered with `{key}` in the compact path notation.
|
|
356
|
+
|
|
357
|
+
```scala
|
|
358
|
+
final case class SchemaError(errors: ::[SchemaError.Single]) {
|
|
359
|
+
def atKey(key: DynamicValue): SchemaError
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
```scala
|
|
364
|
+
import zio.blocks.schema.{DynamicValue, SchemaError}
|
|
365
|
+
|
|
366
|
+
val key = DynamicValue.string("config")
|
|
367
|
+
val err = SchemaError("missing required entry").atKey(key)
|
|
368
|
+
println(err.message) // missing required entry at: {"config"}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Path Chaining
|
|
372
|
+
|
|
373
|
+
All path methods can be chained. Each call prepends to the existing path, so the outermost call appears as the leftmost segment in the rendered message.
|
|
374
|
+
|
|
375
|
+
```scala
|
|
376
|
+
import zio.blocks.schema.SchemaError
|
|
377
|
+
|
|
378
|
+
val err = SchemaError("value out of range")
|
|
379
|
+
.atField("amount") // innermost — added first
|
|
380
|
+
.atIndex(0)
|
|
381
|
+
.atCase("Credit")
|
|
382
|
+
.atField("transactions") // outermost — added last
|
|
383
|
+
println(err.message)
|
|
384
|
+
// value out of range at: .transactions<Credit>[0].amount
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Path annotation applies to **every** `Single` inside the error, so combined errors accumulate paths correctly:
|
|
388
|
+
|
|
389
|
+
```scala
|
|
390
|
+
import zio.blocks.schema.SchemaError
|
|
391
|
+
|
|
392
|
+
val error1 = SchemaError.missingField(Nil, "name")
|
|
393
|
+
val error2 = SchemaError.conversionFailed(Nil, "age must be positive")
|
|
394
|
+
val combined = (error1 ++ error2).atField("person")
|
|
395
|
+
|
|
396
|
+
// Both errors now include the "person" field prefix
|
|
397
|
+
println(combined.errors.head.source.nodes.nonEmpty) // true (name)
|
|
398
|
+
println(combined.errors.tail.head.source.nodes.nonEmpty) // true (age)
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
## Subtypes / Variants
|
|
402
|
+
|
|
403
|
+
`SchemaError.Single` is the sealed base trait for every individual failure. Each variant carries a `source: DynamicOptic` and a `message: String`.
|
|
404
|
+
|
|
405
|
+
```
|
|
406
|
+
SchemaError.Single (sealed trait)
|
|
407
|
+
├── SchemaError.IntoError (sealed sub-trait — marks conversion errors)
|
|
408
|
+
│ └── ConversionFailed(source, details, cause: Option[SchemaError])
|
|
409
|
+
├── MissingField(source, fieldName)
|
|
410
|
+
├── DuplicatedField(source, fieldName)
|
|
411
|
+
├── ExpectationMismatch(source, expectation)
|
|
412
|
+
├── UnknownCase(source, caseName)
|
|
413
|
+
└── Message(source, details)
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
| Subtype | Factory | Typical cause |
|
|
417
|
+
|-----------------------|----------------------------------------|-------------------------------------------------------------------------------------------|
|
|
418
|
+
| `ConversionFailed` | `conversionFailed`, `validationFailed` | Type conversion or smart-constructor failure; may carry a nested `SchemaError` as `cause` |
|
|
419
|
+
| `MissingField` | `missingField` | Required field absent in the decoded representation |
|
|
420
|
+
| `DuplicatedField` | `duplicatedField` | Same field key appears more than once |
|
|
421
|
+
| `ExpectationMismatch` | `expectationMismatch` | Wrong `DynamicValue` variant encountered |
|
|
422
|
+
| `UnknownCase` | `unknownCase` | Discriminator names an unrecognised sealed-trait variant |
|
|
423
|
+
| `Message` | `message`, `apply` | Free-form error with an optional path |
|
|
424
|
+
|
|
425
|
+
We can pattern match on `errors` to handle specific failure kinds:
|
|
426
|
+
|
|
427
|
+
```scala
|
|
428
|
+
import zio.blocks.schema.SchemaError
|
|
429
|
+
|
|
430
|
+
val err = SchemaError.missingField(Nil, "email") ++
|
|
431
|
+
SchemaError.conversionFailed(Nil, "age must be positive")
|
|
432
|
+
|
|
433
|
+
err.errors.foreach {
|
|
434
|
+
case SchemaError.MissingField(source, fieldName) =>
|
|
435
|
+
println(s"Missing '$fieldName' at ${source.toScalaString}")
|
|
436
|
+
case SchemaError.ConversionFailed(source, details, _) =>
|
|
437
|
+
println(s"Conversion failed: $details")
|
|
438
|
+
case other =>
|
|
439
|
+
println(other.message)
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### `SchemaError.IntoError`
|
|
444
|
+
|
|
445
|
+
`IntoError` is a sealed sub-trait of `Single` that marks errors produced during `Into` (type conversion) operations. Its only current implementation is `ConversionFailed`. Codec code pattern-matches on `IntoError` to distinguish conversion errors from structural schema errors:
|
|
446
|
+
|
|
447
|
+
```scala
|
|
448
|
+
sealed trait IntoError extends SchemaError.Single {
|
|
449
|
+
def source: DynamicOptic
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### `SchemaError.ConversionFailed`
|
|
454
|
+
|
|
455
|
+
Represents a failed type or value conversion. When a `cause: Option[SchemaError]` is present, the rendered `message` includes a "Caused by:" section showing the nested failures.
|
|
456
|
+
|
|
457
|
+
```scala
|
|
458
|
+
import zio.blocks.schema.SchemaError
|
|
459
|
+
|
|
460
|
+
// Single nested cause
|
|
461
|
+
val inner1 = SchemaError.conversionFailed(Nil, "name is blank")
|
|
462
|
+
val outer1 = SchemaError.conversionFailed("Person construction failed", inner1)
|
|
463
|
+
println(outer1.message)
|
|
464
|
+
// Person construction failed
|
|
465
|
+
// Caused by: name is blank
|
|
466
|
+
|
|
467
|
+
// Multiple nested causes
|
|
468
|
+
val inner2 = SchemaError.conversionFailed(Nil, "name is blank") ++
|
|
469
|
+
SchemaError.conversionFailed(Nil, "age is negative")
|
|
470
|
+
val outer2 = SchemaError.conversionFailed("Person construction failed", inner2)
|
|
471
|
+
println(outer2.message)
|
|
472
|
+
// Person construction failed
|
|
473
|
+
// Caused by:
|
|
474
|
+
// - name is blank
|
|
475
|
+
// - age is negative
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
## Integration
|
|
479
|
+
|
|
480
|
+
### With Schema Decoding
|
|
481
|
+
|
|
482
|
+
`Schema#fromDynamicValue` returns `Either[SchemaError, A]`. Every codec accumulates path nodes during decoding and calls `atField`, `atIndex`, or `atCase` as it unwinds, producing a fully-annotated `SchemaError` on failure.
|
|
483
|
+
|
|
484
|
+
```scala
|
|
485
|
+
import zio.blocks.schema.{Schema, SchemaError}
|
|
486
|
+
|
|
487
|
+
case class Person(name: String, age: Int)
|
|
488
|
+
|
|
489
|
+
object Person {
|
|
490
|
+
implicit val schema: Schema[Person] = Schema.derived
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
val result: Either[SchemaError, Person] =
|
|
494
|
+
Schema[Person].fromDynamicValue(Schema[Person].toDynamicValue(Person("Alice", 30)))
|
|
495
|
+
|
|
496
|
+
result match {
|
|
497
|
+
case Right(person) => println(s"Decoded: $person")
|
|
498
|
+
case Left(err) => println(s"Error:\n${err.message}")
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
See [Schema](./schema.md) and [DynamicValue](./dynamic-value.md) for the full encoding and decoding API.
|
|
503
|
+
|
|
504
|
+
### With Schema#transform
|
|
505
|
+
|
|
506
|
+
`Schema#transform` accepts `to` and `from` functions that can throw `SchemaError` to signal validation failures during encoding or decoding. We use `SchemaError.validationFailed` (which wraps a `ConversionFailed`) to turn a smart-constructor rejection into a structured schema error:
|
|
507
|
+
|
|
508
|
+
```scala
|
|
509
|
+
import zio.blocks.schema.{Schema, SchemaError}
|
|
510
|
+
|
|
511
|
+
case class PositiveInt private (value: Int)
|
|
512
|
+
|
|
513
|
+
object PositiveInt {
|
|
514
|
+
def make(n: Int): PositiveInt =
|
|
515
|
+
if (n > 0) PositiveInt(n)
|
|
516
|
+
else throw SchemaError.validationFailed("must be positive")
|
|
517
|
+
|
|
518
|
+
implicit val schema: Schema[PositiveInt] =
|
|
519
|
+
Schema[Int].transform(make, _.value)
|
|
520
|
+
}
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
When `make` throws a `SchemaError`, the codec catches it, preserves the full error (including any path already annotated), and surfaces it as `Left(schemaError)` from `Schema#fromDynamicValue` or `Codec#decode`. See [Schema](./schema.md) for the full `transform` API.
|
|
524
|
+
|
|
525
|
+
A runnable version of this example, including composite types and error aggregation, is available in the `schema-examples` module:
|
|
526
|
+
|
|
527
|
+
```bash
|
|
528
|
+
sbt "schema-examples/runMain schemaerror.SchemaErrorExample"
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### With Validation
|
|
532
|
+
|
|
533
|
+
The [Validation](./validation.md) system uses `SchemaError` to report constraint violations. When a `PrimitiveType` carries a `Validation` and the value fails the check, the codec surfaces a `SchemaError.ConversionFailed` at the appropriate path.
|
|
534
|
+
|
|
535
|
+
### With DynamicOptic and Optics
|
|
536
|
+
|
|
537
|
+
Path annotation (`atField`, `atIndex`, `atKey`, `atCase`) builds a [`DynamicOptic`](./dynamic-optic.md) inside each error. Operations such as `DynamicValue#setOrFail` and `DynamicValue#modifyAtPathOrFail` return `Either[SchemaError, DynamicValue]`, using the same factory methods.
|
|
538
|
+
|
|
539
|
+
```scala
|
|
540
|
+
import zio.blocks.schema.{DynamicOptic, DynamicValue, Schema, SchemaError}
|
|
541
|
+
|
|
542
|
+
implicit val intSchema: Schema[Int] = Schema[Int]
|
|
543
|
+
val data = DynamicValue.Sequence(DynamicValue.int(1), DynamicValue.int(2))
|
|
544
|
+
val optic = DynamicOptic.root.at(10)
|
|
545
|
+
|
|
546
|
+
val result: Either[SchemaError, DynamicValue] = data.setOrFail(optic, DynamicValue.int(99))
|
|
547
|
+
result match {
|
|
548
|
+
case Left(err) => println(err.message) // index out of range or similar
|
|
549
|
+
case Right(v) => println(v)
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### As an Exception
|
|
554
|
+
|
|
555
|
+
Because `SchemaError` extends `Exception`, it can be thrown and caught with standard try/catch (In functional code, prefer `Either[SchemaError, A]` or `Option[SchemaError]` instead):
|
|
556
|
+
|
|
557
|
+
```scala
|
|
558
|
+
import zio.blocks.schema.SchemaError
|
|
559
|
+
|
|
560
|
+
try {
|
|
561
|
+
throw SchemaError("Unexpected data shape")
|
|
562
|
+
} catch {
|
|
563
|
+
case e: SchemaError => println(s"Caught schema error: ${e.getMessage}")
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
:::note
|
|
568
|
+
`SchemaError` extends `scala.util.control.NoStackTrace`. Stack traces are suppressed for performance — error location is conveyed through the `DynamicOptic` path inside each `Single`, not the JVM stack.
|
|
569
|
+
:::
|
package/reference/schema-expr.md
CHANGED
|
@@ -70,13 +70,13 @@ val result: Either[OpticCheck, Seq[Boolean]] = combined.eval(alice)
|
|
|
70
70
|
## Installation
|
|
71
71
|
|
|
72
72
|
```scala
|
|
73
|
-
libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.
|
|
73
|
+
libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.27"
|
|
74
74
|
```
|
|
75
75
|
|
|
76
76
|
For cross-platform (Scala.js):
|
|
77
77
|
|
|
78
78
|
```scala
|
|
79
|
-
libraryDependencies += "dev.zio" %%% "zio-blocks-schema" % "0.0.
|
|
79
|
+
libraryDependencies += "dev.zio" %%% "zio-blocks-schema" % "0.0.27"
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
Supported Scala versions: 2.13.x and 3.x.
|
package/reference/schema.md
CHANGED
|
@@ -618,3 +618,32 @@ object UserId {
|
|
|
618
618
|
.transform(UserId(_), _.value)
|
|
619
619
|
}
|
|
620
620
|
```
|
|
621
|
+
|
|
622
|
+
## Compile-Time Shape Constraints (`Allows`)
|
|
623
|
+
|
|
624
|
+
ZIO Blocks provides `Allows[A, S]` — a phantom-typed capability token that proves, at compile time, that type `A` satisfies the structural grammar `S`. This lets library authors express and enforce structural preconditions on their generic APIs without writing macros themselves.
|
|
625
|
+
|
|
626
|
+
```scala
|
|
627
|
+
import zio.blocks.schema.Schema
|
|
628
|
+
import zio.blocks.schema.comptime.Allows
|
|
629
|
+
import Allows._
|
|
630
|
+
|
|
631
|
+
// Require a flat record of scalars (e.g. for CSV or RDBMS)
|
|
632
|
+
def writeCsv[A: Schema](rows: Seq[A])(using
|
|
633
|
+
Allows[A, Record[Primitive | Optional[Primitive]]]
|
|
634
|
+
): Unit = ???
|
|
635
|
+
|
|
636
|
+
// Sealed traits auto-unwrap: each case must satisfy Record[...] — no Variant node needed
|
|
637
|
+
def publish[A: Schema](event: A)(using
|
|
638
|
+
Allows[A, Record[Primitive | Sequence[Primitive]]]
|
|
639
|
+
): Unit = ???
|
|
640
|
+
|
|
641
|
+
// Recursive grammar (e.g. for a JSON document store)
|
|
642
|
+
def toJson[A: Schema](doc: A)(using
|
|
643
|
+
Allows[A, Record[Primitive | Self | Optional[Primitive | Self] | Sequence[Primitive | Self]]]
|
|
644
|
+
): String = ???
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
When a type does not satisfy the grammar, the user gets a precise compile-time error naming the violating field and suggesting a fix. No runtime surprises.
|
|
648
|
+
|
|
649
|
+
See the [`Allows` reference](./allows.md) for the full grammar node table, union syntax, `Self` for recursive types, newtypes, and error message examples.
|