@zio.dev/zio-blocks 0.0.26 → 0.0.28

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/reference/json.md CHANGED
@@ -828,7 +828,7 @@ Type ordering: Null < Boolean < Number < String < Array < Object
828
828
 
829
829
  ## JSON Diffing
830
830
 
831
- `JsonDiffer` computes the difference between two JSON values, producing a `JsonPatch` that transforms the source into the target:
831
+ `JsonDiffer` computes the difference between two JSON values, producing a [`JsonPatch`](./json-patch.md) that transforms the source into the target:
832
832
 
833
833
  ```scala
834
834
  import zio.blocks.schema.json.{Json, JsonPatch}
@@ -75,13 +75,13 @@ textAny.matches(html) // true
75
75
  Add the following to your `build.sbt`:
76
76
 
77
77
  ```scala
78
- libraryDependencies += "dev.zio" %% "zio-blocks-mediatype" % "0.0.26"
78
+ libraryDependencies += "dev.zio" %% "zio-blocks-mediatype" % "0.0.28"
79
79
  ```
80
80
 
81
81
  For cross-platform projects (Scala.js):
82
82
 
83
83
  ```scala
84
- libraryDependencies += "dev.zio" %%% "zio-blocks-mediatype" % "0.0.26"
84
+ libraryDependencies += "dev.zio" %%% "zio-blocks-mediatype" % "0.0.28"
85
85
  ```
86
86
 
87
87
  Supported Scala versions: 2.13.x and 3.x.
@@ -38,12 +38,12 @@ object User extends CompanionOptics[User] {
38
38
  .derived[User]
39
39
  .modifier(Modifier.config("db.table-name", "users"))
40
40
 
41
- implicit val jsonCodec: JsonBinaryCodec[User] =
42
- schema
43
- .deriving[JsonBinaryCodec](JsonBinaryCodecDeriver)
44
- .modifier(User.name, Modifier.rename("username"))
45
- .modifier(User.cache, Modifier.transient())
46
- .derive
41
+ implicit val jsonCodec: JsonBinaryCodec[User] =
42
+ schema
43
+ .deriving(JsonBinaryCodecDeriver)
44
+ .modifier(User.name, Modifier.rename("username"))
45
+ .modifier(User.cache, Modifier.transient())
46
+ .derive
47
47
 
48
48
  lazy val id : Lens[User, String] = $(_.id)
49
49
  lazy val name : Lens[User, String] = $(_.name)
@@ -86,9 +86,9 @@ object User extends CompanionOptics[User] {
86
86
  implicit val schema: Schema[User] =
87
87
  Schema.derived[User]
88
88
 
89
- implicit val jsonCodec: JsonBinaryCodec[User] =
90
- schema
91
- .derive[JsonBinaryCodec](JsonBinaryCodecDeriver)
89
+ implicit val jsonCodec: JsonBinaryCodec[User] =
90
+ schema
91
+ .derive(JsonBinaryCodecDeriver)
92
92
  }
93
93
  ```
94
94
 
@@ -15,6 +15,10 @@ A `Patch[S]` represents a sequence of operations that transform a value of type
15
15
  - **Optimistic Updates** — Apply patches locally while syncing with a server
16
16
  - **Schema Evolution** — Patches work with the schema system, adapting as data structures evolve
17
17
 
18
+ :::note
19
+ For **untyped JSON patching** without a schema, use [`JsonPatch`](./json-patch.md) instead. `JsonPatch` is optimized for diff-and-apply workflows on raw JSON values and provides compact delta representations without requiring typed optics.
20
+ :::
21
+
18
22
  ```scala
19
23
  import zio.blocks.schema._
20
24
  import zio.blocks.schema.patch._
@@ -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
+ :::