@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.
@@ -0,0 +1,1195 @@
1
+ ---
2
+ id: zio-schema-migration
3
+ title: "Migrating from ZIO Schema to ZIO Blocks Schema"
4
+ ---
5
+
6
+ This guide helps you migrate an application that uses [ZIO Schema](https://github.com/zio/zio-schema) (version 1.x) to [ZIO Blocks Schema](https://github.com/zio/zio-blocks) (the schema module of ZIO Blocks). It covers the conceptual differences between the two libraries, provides a systematic mapping of data types, and shows how to rewrite the most common patterns in the idiomatic ZIO Blocks style.
7
+
8
+ **What we will cover:**
9
+
10
+ - Prerequisites and dependency changes
11
+ - The core architectural shift from `Schema[A]` as a sealed trait to `Schema[A]` as a thin wrapper over `Reflect[F, A]`
12
+ - Migrating schema definitions for primitives, records, enums, collections, optional values, and newtypes
13
+ - Replacing the annotation/modifier system
14
+ - Adapting codec derivation to the unified `Format + Deriver` model
15
+ - Replacing `DynamicValue` usage
16
+ - Migrating optics and accessor patterns
17
+ - Migrating diff, patch, and schema evolution patterns
18
+ - Handling types and features that no longer have a direct analogue
19
+
20
+ ---
21
+
22
+ ## Prerequisites
23
+
24
+ ### Dependency Changes
25
+
26
+ Replace the ZIO Schema dependency group with ZIO Blocks Schema:
27
+
28
+ **Before (ZIO Schema 1.x):**
29
+
30
+ ```scala
31
+ libraryDependencies += "dev.zio" %% "zio-schema" % "1.x.x"
32
+ libraryDependencies += "dev.zio" %% "zio-schema-derivation" % "1.x.x"
33
+ libraryDependencies += "dev.zio" %% "zio-schema-json" % "1.x.x"
34
+ libraryDependencies += "dev.zio" %% "zio-schema-protobuf" % "1.x.x"
35
+ libraryDependencies += "dev.zio" %% "zio-schema-avro" % "1.x.x"
36
+ ```
37
+
38
+ **After (ZIO Blocks Schema):**
39
+
40
+ ```scala
41
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.28"
42
+ // Optional codec modules:
43
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema-avro" % "0.0.28"
44
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema-thrift" % "0.0.28"
45
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema-bson" % "0.0.28"
46
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema-messagepack" % "0.0.28"
47
+ libraryDependencies += "dev.zio" %% "zio-blocks-schema-toon" % "0.0.28"
48
+ ```
49
+
50
+ Key points:
51
+ - JSON codec support is now built into `zio-blocks-schema` — no separate JSON module.
52
+ - There is no separate `zio-blocks-schema-derivation` dependency; derivation is built in.
53
+ - The `scala-reflect` provided dependency (required in ZIO Schema for Scala 2) is still needed for Scala 2 macro derivation — add it the same way as before.
54
+ - ZIO Blocks Schema has **zero runtime dependency on ZIO itself**. You do not need `zio` on your classpath for schema operations.
55
+
56
+ ### Package Rename
57
+
58
+ All imports change from `zio.schema` to `zio.blocks.schema`:
59
+
60
+ ```scala
61
+ // Before
62
+ import zio.schema._
63
+ import zio.schema.annotation._
64
+ import zio.schema.codec._
65
+ import zio.schema.meta._
66
+
67
+ // After
68
+ import zio.blocks.schema._
69
+ import zio.blocks.schema.binding._
70
+ import zio.blocks.schema.derive._
71
+ import zio.blocks.schema.patch._
72
+ import zio.blocks.schema.json._
73
+ ```
74
+
75
+ ---
76
+
77
+ ## The Core Architecture Shift
78
+
79
+ The most important thing to understand when migrating is that `Schema[A]` is no longer a sealed trait hierarchy — it is a thin case class:
80
+
81
+ ```scala
82
+ // ZIO Schema 1.x: Schema is a sealed trait with ~20 concrete cases
83
+ sealed trait Schema[A] {
84
+ def annotations: Chunk[Any]
85
+ def defaultValue: Either[String, A]
86
+ // ...
87
+ }
88
+
89
+ // ZIO Blocks Schema: Schema is a case class wrapping Reflect
90
+ final case class Schema[A](reflect: Reflect.Bound[A])
91
+ ```
92
+
93
+ The structural description lives in `Reflect[F[_, _], A]`, a sealed trait with eight node types. The `F` type parameter distinguishes a *bound* reflect (with runtime constructors and deconstructors) from an *unbound* one (structural information only):
94
+
95
+ ```
96
+ Reflect[F, A]
97
+ ├── Reflect.Record[F, A] — case classes and other product types
98
+ ├── Reflect.Variant[F, A] — sealed traits, enums, Option, Either
99
+ ├── Reflect.Sequence[F, A, C[_]] — List, Vector, Set, Chunk, etc.
100
+ ├── Reflect.Map[F, K, V, M[_,_]] — Map[K, V]
101
+ ├── Reflect.Primitive[F, A] — Int, String, UUID, java.time.*, etc.
102
+ ├── Reflect.Wrapper[F, A, B] — opaque types and validated newtypes
103
+ ├── Reflect.Dynamic[F] — escape hatch for schema-agnostic data
104
+ └── Reflect.Deferred[F, A] — recursive (self-referential) types
105
+ ```
106
+
107
+ As a result, you will rarely pattern match on `Schema[A]` directly — instead, you work through `schema.reflect` when you need to inspect structure.
108
+
109
+ ---
110
+
111
+ ## Migrating Schema Definitions
112
+
113
+ ### Schema Derivation
114
+
115
+ Automatic derivation syntax is essentially unchanged:
116
+
117
+ ```scala
118
+ // ZIO Schema 1.x — Scala 2: type inferred from ascription; Scala 3: type param required
119
+ import zio.schema._
120
+ final case class Person(name: String, age: Int)
121
+ object Person {
122
+ implicit val schema: Schema[Person] = DeriveSchema.gen[Person]
123
+ }
124
+
125
+ // ZIO Blocks Schema — identical call in Scala 2 and Scala 3
126
+ import zio.blocks.schema._
127
+ final case class Person(name: String, age: Int)
128
+ object Person {
129
+ implicit val schema: Schema[Person] = Schema.derived[Person]
130
+ }
131
+ ```
132
+
133
+ `Schema.derived[A]` works identically in both Scala 2 and Scala 3 in ZIO Blocks Schema. There is no separate `DeriveSchema` import and no arity limit.
134
+
135
+ ### Primitives
136
+
137
+ All 30 primitive types from ZIO Schema are present in ZIO Blocks Schema with the same coverage: `Unit`, `Boolean`, `Byte`, `Short`, `Int`, `Long`, `Float`, `Double`, `Char`, `String`, `BigInt`, `BigDecimal`, all `java.time.*` types, `Currency`, and `UUID`.
138
+
139
+ Implicit schemas are available in the same way:
140
+
141
+ ```scala
142
+ // ZIO Schema 1.x
143
+ val s: Schema[Int] = Schema[Int]
144
+ val s: Schema[java.time.Instant] = Schema[java.time.Instant]
145
+
146
+ // ZIO Blocks Schema — identical call sites
147
+ val s: Schema[Int] = Schema[Int]
148
+ val s: Schema[java.time.Instant] = Schema[java.time.Instant]
149
+ ```
150
+
151
+ The underlying representation changes: ZIO Schema uses `Schema.Primitive[A](standardType: StandardType[A])`, while ZIO Blocks Schema uses `Reflect.Primitive[F, A](primitiveType: PrimitiveType[A], ...)`. Both carry default values and ordering, but in ZIO Blocks the primitive type also carries an embedded `Validation[A]` constraint (see the [Migrating Validation](#migrating-validation) section below).
152
+
153
+ ### Records (Case Classes)
154
+
155
+ **Before (ZIO Schema 1.x):**
156
+
157
+ ```scala
158
+ import zio.schema._
159
+
160
+ final case class Address(street: String, city: String, postCode: String)
161
+ final case class Person(name: String, age: Int, address: Address)
162
+
163
+ object Person {
164
+ implicit val schema: Schema[Person] = DeriveSchema.gen
165
+ }
166
+ ```
167
+
168
+ **After (ZIO Blocks Schema):**
169
+
170
+ ```scala
171
+ import zio.blocks.schema._
172
+
173
+ final case class Address(street: String, city: String, postCode: String)
174
+ final case class Person(name: String, age: Int, address: Address)
175
+
176
+ object Person {
177
+ implicit val schema: Schema[Person] = Schema.derived[Person]
178
+ }
179
+ ```
180
+
181
+ The derivation call is identical. Internally, ZIO Blocks generates a `Reflect.Record` node (a single generic type, not the arity-specialised `CaseClass1`..`CaseClass22` of ZIO Schema), so there is no 22-field arity limit.
182
+
183
+ If you were writing schemas manually using `Schema.CaseClass2[...]` or similar, you will need to rewrite those. The equivalent in ZIO Blocks is to write the `Reflect.Record` directly or, preferably, just use `Schema.derived[A]`:
184
+
185
+ ```scala
186
+ // ZIO Schema 1.x — manual construction for a 2-field record
187
+ val personSchema: Schema[Person] =
188
+ Schema.CaseClass2[String, Int, Person](
189
+ id0 = TypeId.fromTypeName("Person"),
190
+ field01 = Schema.Field("name", Schema[String], get0 = _.name, set0 = (p, v) => p.copy(name = v)),
191
+ field02 = Schema.Field("age", Schema[Int], get0 = _.age, set0 = (p, v) => p.copy(age = v)),
192
+ construct0 = Person(_, _)
193
+ )
194
+
195
+ // ZIO Blocks Schema — prefer derivation; no manual CaseClass* required
196
+ val personSchema: Schema[Person] = Schema.derived[Person]
197
+ ```
198
+
199
+ Manual construction is still possible in ZIO Blocks Schema (by assembling a `Reflect.Record` directly), but it is substantially more involved because you must supply a `Binding.Record` with explicit `Constructor[A]` and `Deconstructor[A]` implementations that use the unboxed register system. Automatic derivation is strongly preferred.
200
+
201
+ ### Sealed Traits / Enums (Sum Types)
202
+
203
+ **Before (ZIO Schema 1.x):**
204
+
205
+ ```scala
206
+ import zio.schema._
207
+
208
+ sealed trait Shape
209
+ case class Circle(radius: Double) extends Shape
210
+ case class Rectangle(width: Double, height: Double) extends Shape
211
+
212
+ object Shape {
213
+ implicit val schema: Schema[Shape] = DeriveSchema.gen
214
+ }
215
+ ```
216
+
217
+ **After (ZIO Blocks Schema):**
218
+
219
+ ```scala
220
+ import zio.blocks.schema._
221
+
222
+ sealed trait Shape
223
+ case class Circle(radius: Double) extends Shape
224
+ case class Rectangle(width: Double, height: Double) extends Shape
225
+
226
+ object Shape {
227
+ implicit val schema: Schema[Shape] = Schema.derived[Shape]
228
+ }
229
+ ```
230
+
231
+ Again, the call is identical. Internally, ZIO Blocks generates a `Reflect.Variant` node. There is no 22-case arity limit.
232
+
233
+ ### Optional Values
234
+
235
+ ZIO Schema has a first-class `Schema.Optional[A]` node. In ZIO Blocks, `Option[A]` is modeled as a `Reflect.Variant` with two cases (`None` and `Some`). From a user perspective this is transparent — implicit schemas for `Option[A]` exist in the same form:
236
+
237
+ ```scala
238
+ // ZIO Schema 1.x
239
+ val optSchema: Schema[Option[String]] = Schema[Option[String]]
240
+
241
+ // ZIO Blocks Schema — identical
242
+ val optSchema: Schema[Option[String]] = Schema[Option[String]]
243
+ ```
244
+
245
+ For value-type `Option` variants (e.g., `Option[Int]`), ZIO Blocks provides specialised implicit instances (`Schema.optionInt`, `Schema.optionLong`, etc.) that avoid boxing. These are resolved automatically by the compiler — no code change required.
246
+
247
+ :::tip
248
+ The internal modeling difference (variant vs. dedicated node) is only relevant if you are pattern-matching on the raw `Schema` or `Reflect` structure. In that case, replace any match on `Schema.Optional(inner, _)` with a check on `reflect.isOption` and use `reflect.optionInnerType` to retrieve the inner reflect:
249
+
250
+ ```scala
251
+ // ZIO Schema 1.x — pattern matching on Optional
252
+ schema match {
253
+ case Schema.Optional(inner, _) => // use inner
254
+ case _ => // ...
255
+ }
256
+
257
+ // ZIO Blocks Schema — use the isOption predicate
258
+ // optionInnerType returns Option[Reflect[F, ?]] where F matches the enclosing Reflect's binding
259
+ val r = schema.reflect
260
+ if (r.isOption) {
261
+ val inner: Option[Reflect[binding.Binding, ?]] = r.optionInnerType
262
+ // use inner
263
+ }
264
+ ```
265
+ :::
266
+
267
+ ### Either
268
+
269
+ In ZIO Schema, `Either[A, B]` is a first-class `Schema.Either[A, B]` node. In ZIO Blocks, it is modeled as a two-case `Reflect.Variant`. The implicit schema is provided automatically:
270
+
271
+ ```scala
272
+ // ZIO Schema 1.x
273
+ val eitherSchema: Schema[Either[String, Int]] = Schema.either[String, Int]
274
+
275
+ // ZIO Blocks Schema
276
+ val eitherSchema: Schema[Either[String, Int]] = Schema[Either[String, Int]]
277
+ ```
278
+
279
+ :::warning
280
+ ZIO Blocks Schema does not have a `Fallback[A, B]` type. If you were using `Schema.Fallback` for partial decoding, you will need to model that with a custom `Reflect.Variant` or handle it in your codec logic directly.
281
+ :::
282
+
283
+ ### Collections
284
+
285
+ All standard collection types are supported with the same implicit schema pattern:
286
+
287
+ ```scala
288
+ // ZIO Schema 1.x
289
+ Schema[List[String]]
290
+ Schema[Vector[Int]]
291
+ Schema[Chunk[Double]]
292
+ Schema[Set[String]]
293
+ Schema[Map[String, Int]]
294
+
295
+ // ZIO Blocks Schema — identical call sites
296
+ Schema[List[String]]
297
+ Schema[Vector[Int]]
298
+ Schema[Chunk[Double]] // uses zio.blocks.chunk.Chunk
299
+ Schema[Set[String]]
300
+ Schema[Map[String, Int]]
301
+ ```
302
+
303
+ Note that `Chunk` is now `zio.blocks.chunk.Chunk` (not `zio.Chunk`). This is a zero-dependency replacement with the same API surface for typical usage.
304
+
305
+ ZIO Schema's `NonEmptyChunk` and `NonEmptyMap` schemas do not have direct equivalents in ZIO Blocks Schema. The recommended approach is to model them as wrapper types:
306
+
307
+ ```scala
308
+ // ZIO Schema 1.x — NonEmptyChunk implicit schema
309
+ val schema: Schema[NonEmptyChunk[String]] = Schema[NonEmptyChunk[String]]
310
+
311
+ // ZIO Blocks Schema — model as a validated wrapper
312
+ import zio.blocks.schema._
313
+
314
+ final case class NonEmptyList[A] private (values: List[A])
315
+ object NonEmptyList {
316
+ def apply[A](head: A, tail: A*): NonEmptyList[A] = new NonEmptyList(head :: tail.toList)
317
+
318
+ implicit def schema[A](implicit element: Schema[A]): Schema[NonEmptyList[A]] =
319
+ Schema[List[A]].transform(
320
+ to = list =>
321
+ if (list.nonEmpty) new NonEmptyList(list)
322
+ else throw SchemaError.validationFailed("List must not be empty"),
323
+ from = _.values
324
+ )
325
+ }
326
+ ```
327
+
328
+ ### Newtypes and Opaque Types
329
+
330
+ ZIO Schema uses `Schema.transform` (which wraps a `Transform` node) for both validated newtypes and lossless wrappers:
331
+
332
+ ```scala
333
+ // ZIO Schema 1.x
334
+ implicit val bigDecimalSchema: Schema[BigDecimal] =
335
+ Schema.primitive[java.math.BigDecimal].transform(BigDecimal(_), _.bigDecimal)
336
+ ```
337
+
338
+ ZIO Blocks Schema has `Schema[A].transform(to: A => B, from: B => A)` which produces a `Reflect.Wrapper` node. The `to` and `from` functions are total but can throw to indicate failure. The method also requires an implicit `TypeId[B]`, which is derived automatically by the macro system for any concrete named type — you will not need to supply it manually for ordinary case classes:
339
+
340
+ ```scala
341
+ // ZIO Blocks Schema
342
+ case class Email(value: String)
343
+ object Email {
344
+ // TypeId[Email] is resolved implicitly from the macro-derived instance
345
+ implicit val schema: Schema[Email] =
346
+ Schema[String].transform(
347
+ to = str =>
348
+ if (str.contains('@')) Email(str)
349
+ else throw SchemaError.validationFailed("Not a valid email address"),
350
+ from = _.value
351
+ )
352
+ }
353
+ ```
354
+
355
+ For simple lossless wrappers where no validation is needed, the pattern is the same but without the error throw:
356
+
357
+ ```scala
358
+ // ZIO Blocks Schema — simple newtype wrapper
359
+ case class UserId(value: Long)
360
+ object UserId {
361
+ implicit val schema: Schema[UserId] =
362
+ Schema[Long].transform(UserId(_), _.value)
363
+ }
364
+ ```
365
+
366
+ :::warning
367
+ In ZIO Schema, `transformOrFail` accepted `A => Either[String, B]` return types. In ZIO Blocks, `transform` uses total functions that throw on failure — use `throw SchemaError.validationFailed(message)` in the `to` function to signal failure. There is no `transformOrFail` method.
368
+
369
+ If you encounter a "could not find implicit value for parameter typeId: TypeId[B]" error, ensure the target type `B` is a concrete, named class or object (not an anonymous structural type or a type alias to a primitive). For primitive-backed aliases such as `type Meters = Double`, wrap in a `case class` instead.
370
+ :::
371
+
372
+ ### Lazy / Recursive Schemas
373
+
374
+ **Before (ZIO Schema 1.x):**
375
+
376
+ ```scala
377
+ import zio.schema._
378
+
379
+ case class Tree(value: Int, children: List[Tree])
380
+ object Tree {
381
+ implicit lazy val schema: Schema[Tree] = DeriveSchema.gen
382
+ // Or manually with Schema.defer:
383
+ // implicit lazy val schema: Schema[Tree] = Schema.CaseClass2(
384
+ // ..., field02 = Schema.Field("children", Schema.defer(Schema.list(schema)), ...)
385
+ // )
386
+ }
387
+ ```
388
+
389
+ **After (ZIO Blocks Schema):**
390
+
391
+ ```scala
392
+ import zio.blocks.schema._
393
+
394
+ case class Tree(value: Int, children: List[Tree])
395
+ object Tree {
396
+ implicit val schema: Schema[Tree] = Schema.derived[Tree]
397
+ }
398
+ ```
399
+
400
+ Recursive types are handled automatically by the macro. Internally, ZIO Blocks generates a `Reflect.Deferred` node that uses thread-local cycle detection — you do not need to use `Schema.defer` manually. The `implicit val` (not `lazy val`) is sufficient.
401
+
402
+ If you were wrapping a recursive reference manually with `Schema.defer(...)`, simply remove that wrapper — recursive references inside `Schema.derived` are handled for you.
403
+
404
+ ---
405
+
406
+ ## Migrating Annotations and Modifiers
407
+
408
+ ZIO Schema uses an open `Chunk[Any]` annotation system. ZIO Blocks Schema replaces this with a strongly-typed, sealed `Modifier` hierarchy.
409
+
410
+ ### Transient Fields
411
+
412
+ ```scala
413
+ // ZIO Schema 1.x
414
+ import zio.schema.annotation._
415
+
416
+ final case class User(name: String, @transientField password: String)
417
+ object User {
418
+ implicit val schema: Schema[User] = DeriveSchema.gen
419
+ }
420
+
421
+ // ZIO Blocks Schema
422
+ import zio.blocks.schema._
423
+
424
+ // Transient fields must have a default value in ZIO Blocks Schema.
425
+ // Because transient fields are excluded from serialization, the decoder
426
+ // needs a default to reconstruct the object without that field in the input.
427
+ final case class User(name: String, @Modifier.transient() password: String = "")
428
+ object User {
429
+ implicit val schema: Schema[User] = Schema.derived[User]
430
+ }
431
+ ```
432
+
433
+ ### Field Renaming
434
+
435
+ ```scala
436
+ // ZIO Schema 1.x — @fieldName annotation
437
+ import zio.schema.annotation._
438
+
439
+ final case class Product(@fieldName("product_name") name: String, price: Double)
440
+
441
+ // ZIO Blocks Schema — @Modifier.rename annotation
442
+ import zio.blocks.schema._
443
+
444
+ final case class Product(@Modifier.rename("product_name") name: String, price: Double)
445
+ ```
446
+
447
+ ### Field Aliases (for Decoding)
448
+
449
+ ```scala
450
+ // ZIO Schema 1.x — @fieldNameAliases annotation
451
+ import zio.schema.annotation._
452
+
453
+ final case class Config(@fieldNameAliases("max-size", "max_size") maxSize: Int)
454
+
455
+ // ZIO Blocks Schema — @Modifier.alias annotation (one alias per annotation)
456
+ import zio.blocks.schema._
457
+
458
+ final case class Config(
459
+ @Modifier.alias("max-size")
460
+ @Modifier.alias("max_size")
461
+ maxSize: Int
462
+ )
463
+ ```
464
+
465
+ ### Codec-Specific Configuration
466
+
467
+ ```scala
468
+ // ZIO Schema 1.x — no standard mechanism; each codec module defines its own
469
+ // e.g., @fieldDefaultValue, @optionalField, or codec-specific annotations
470
+
471
+ // ZIO Blocks Schema — use @Modifier.config with convention "format.property"
472
+ import zio.blocks.schema._
473
+
474
+ final case class Message(
475
+ @Modifier.config("protobuf.field-id", "1") id: Long,
476
+ @Modifier.config("protobuf.field-id", "2") content: String
477
+ )
478
+ ```
479
+
480
+ ### Discriminator and Case Name Annotations
481
+
482
+ ```scala
483
+ // ZIO Schema 1.x
484
+ import zio.schema.annotation._
485
+
486
+ @discriminatorName("type")
487
+ sealed trait Event
488
+ @caseName("user_created")
489
+ case class UserCreated(userId: String) extends Event
490
+
491
+ // ZIO Blocks Schema — use Modifier.rename on the case, Modifier.config for discriminator
492
+ import zio.blocks.schema._
493
+
494
+ sealed trait Event
495
+ @Modifier.rename("user_created")
496
+ case class UserCreated(userId: String) extends Event
497
+ ```
498
+
499
+ For discriminator key configuration on the enclosing sealed trait, use `Modifier.config` on the reflect node after derivation:
500
+
501
+ ```scala
502
+ implicit val schema: Schema[Event] =
503
+ Schema.derived[Event].modifier(Modifier.config("json.discriminator", "type"))
504
+ ```
505
+
506
+ ### Programmatic Annotation
507
+
508
+ ZIO Schema allows adding annotations at any time via `schema.annotate(annotation)`. In ZIO Blocks, you add modifiers:
509
+
510
+ ```scala
511
+ // ZIO Schema 1.x
512
+ val schema2 = schema.annotate(someAnnotation)
513
+
514
+ // ZIO Blocks Schema
515
+ val schema2 = schema.modifier(Modifier.config("key", "value"))
516
+ ```
517
+
518
+ ---
519
+
520
+ ## Migrating Codec Derivation
521
+
522
+ ### The Unified Format Model
523
+
524
+ ZIO Schema has separate codec APIs in each codec sub-module (e.g., `JsonCodec.jsonCodec`, `ProtobufCodec.protobufCodec`). ZIO Blocks Schema introduces a unified `codec.Format` interface that all codec modules implement. Codecs are derived via a consistent call:
525
+
526
+ ```scala
527
+ // ZIO Schema 1.x — each codec module has its own factory
528
+ // JsonCodec.jsonCodec returns a zio.json.JsonCodec (a text codec from the zio-json library)
529
+ import zio.schema.codec.JsonCodec
530
+ val jsonCodec = JsonCodec.jsonCodec(Person.schema)
531
+
532
+ // ProtobufCodec.protobufCodec returns a BinaryCodec[A] (Chunk[Byte] in / out)
533
+ import zio.schema.codec.ProtobufCodec
534
+ val protoCodec: BinaryCodec[Person] = ProtobufCodec.protobufCodec(Person.schema)
535
+
536
+ // ZIO Blocks Schema — all codecs via schema.derive(Format); return type inferred
537
+ import zio.blocks.schema._
538
+ import zio.blocks.schema.json.JsonFormat
539
+ val jsonCodec = Person.schema.derive(JsonFormat) // inferred: JsonBinaryCodec[Person]
540
+
541
+ import zio.blocks.schema.avro.AvroFormat
542
+ val avroCodec = Person.schema.derive(AvroFormat)
543
+ ```
544
+
545
+ Derived codecs are cached per `(Schema, Format)` pair — subsequent calls to `schema.derive(JsonFormat)` return the same instance.
546
+
547
+ ### Encoding and Decoding
548
+
549
+ The codec interface differs significantly between the two libraries:
550
+
551
+ ```scala
552
+ // ZIO Schema 1.x — Protobuf (true BinaryCodec: Chunk[Byte] in/out)
553
+ import zio.schema.codec.ProtobufCodec
554
+ val codec: BinaryCodec[Person] = ProtobufCodec.protobufCodec(Person.schema)
555
+ val encoded: Chunk[Byte] = codec.encode(Person("Alice", 30))
556
+ val decoded: Either[DecodeError, Person] = codec.decode(encoded)
557
+
558
+ // ZIO Schema 1.x — JSON (zio-json JsonCodec: String in/out)
559
+ import zio.schema.codec.JsonCodec
560
+ val jsonCodec = JsonCodec.jsonCodec(Person.schema)
561
+ val json: String = jsonCodec.encodeJson(Person("Alice", 30), None).toString
562
+ val fromJson: Either[String, Person] = jsonCodec.decodeJson(json)
563
+ ```
564
+
565
+ ```scala
566
+ // ZIO Blocks Schema — all formats use ByteBuffer (binary) or CharBuffer (text)
567
+ import zio.blocks.schema._
568
+ import zio.blocks.schema.json.JsonFormat
569
+ import java.nio.ByteBuffer
570
+
571
+ val person = Person("Alice", 30)
572
+
573
+ // Encode
574
+ val buffer = ByteBuffer.allocate(1024)
575
+ Person.schema.encode(JsonFormat)(buffer)(person)
576
+
577
+ // Decode
578
+ buffer.flip()
579
+ val result: Either[SchemaError, Person] = Person.schema.decode(JsonFormat)(buffer)
580
+ ```
581
+
582
+ ZIO Blocks codecs use `java.nio.ByteBuffer` for binary formats and `java.nio.CharBuffer` for text formats, and do not depend on `zio-json` or any other external codec library.
583
+
584
+ ### JSON Codec
585
+
586
+ JSON support is built into the core `zio-blocks-schema` module — no separate dependency is needed:
587
+
588
+ ```scala
589
+ // ZIO Schema 1.x — requires a separate zio-schema-json module
590
+ libraryDependencies += "dev.zio" %% "zio-schema-json" % "1.x.x"
591
+ import zio.schema.codec.JsonCodec
592
+ val codec = JsonCodec.jsonCodec(Person.schema) // returns zio.json.JsonCodec
593
+
594
+ // ZIO Blocks Schema — built into zio-blocks-schema; no extra dependency
595
+ import zio.blocks.schema.json.JsonFormat
596
+ val codec = Person.schema.derive(JsonFormat) // returns JsonBinaryCodec[Person]
597
+ ```
598
+
599
+ ### Streaming Codecs
600
+
601
+ ZIO Schema's streaming codec methods (`streamEncoder`, `streamDecoder`) integrated with `ZStream`. ZIO Blocks Schema codecs are format-level `encode`/`decode` operations over `ByteBuffer` or `CharBuffer` — they do not depend on ZIO's streaming primitives. If you need streaming, wrap the codec in your effect system's streaming abstraction.
602
+
603
+ ---
604
+
605
+ ## Migrating DynamicValue
606
+
607
+ ### Structure Changes
608
+
609
+ The `DynamicValue` ADT is significantly simplified in ZIO Blocks — from 15 cases down to 6. The key differences:
610
+
611
+ - ZIO Schema's `DynamicValue.Primitive[A](value: A, standardType: StandardType[A])` stores the raw value and its `StandardType` inline. ZIO Blocks wraps the scalar in a `PrimitiveValue` case class instead.
612
+ - `DynamicValue.Record` drops the `TypeId` parameter and uses `Chunk[(String, DynamicValue)]` instead of `ListMap[String, DynamicValue]`.
613
+ - `Option`, `Either`, and `Tuple` are no longer dedicated ADT cases — they are represented structurally using `Variant` and `Record`.
614
+
615
+ | ZIO Schema | ZIO Blocks Schema |
616
+ |---|---|
617
+ | `Primitive[A](value: A, standardType: StandardType[A])` | `Primitive(value: PrimitiveValue)` |
618
+ | `Record(id: TypeId, values: ListMap[String, DynamicValue])` | `Record(fields: Chunk[(String, DynamicValue)])` — no TypeId |
619
+ | `Enumeration(id: TypeId, value: (String, DynamicValue))` | `Variant(caseName: String, value: DynamicValue)` |
620
+ | `Sequence(values: Chunk[DynamicValue])` | `Sequence(elements: Chunk[DynamicValue])` |
621
+ | `Dictionary(entries: Chunk[(DynamicValue, DynamicValue)])` | `Map(entries: Chunk[(DynamicValue, DynamicValue)])` |
622
+ | `SomeValue(value: DynamicValue)` | `Variant("Some", Record(Chunk("value" -> ...)))` |
623
+ | `NoneValue` | `Variant("None", Null)` |
624
+ | `LeftValue(value: DynamicValue)` | `Variant("Left", Record(Chunk("value" -> ...)))` |
625
+ | `RightValue(value: DynamicValue)` | `Variant("Right", Record(Chunk("value" -> ...)))` |
626
+ | `Tuple(left, right)` | `Record(Chunk("_1" -> left, "_2" -> right))` |
627
+ | `SetValue(values: Set[DynamicValue])` | `Sequence(elements: Chunk[DynamicValue])` |
628
+ | `BothValue(left, right)` | No direct equivalent (used by `Fallback`, which is removed) |
629
+ | `DynamicAst(ast: MetaSchema)` | No direct equivalent |
630
+ | `Singleton[A](instance: A)` | No direct equivalent |
631
+ | `Error(message: String)` | No direct equivalent — use `SchemaError` |
632
+
633
+ ### Primitive Values
634
+
635
+ In ZIO Schema, primitive values are stored inline in `DynamicValue.Primitive[A](value: A, standardType: StandardType[A])`. There is no separate `PrimitiveValue` type. In ZIO Blocks, a sealed `PrimitiveValue` ADT wraps each primitive:
636
+
637
+ ```scala
638
+ // ZIO Schema 1.x — value and StandardType are separate constructor arguments
639
+ import zio.schema.{DynamicValue, StandardType}
640
+ val pv: DynamicValue = DynamicValue.Primitive(42, StandardType[Int])
641
+ val ps: DynamicValue = DynamicValue.Primitive("hello", StandardType[String])
642
+
643
+ // ZIO Blocks Schema — value is wrapped in a PrimitiveValue case class
644
+ import zio.blocks.schema.{DynamicValue, PrimitiveValue}
645
+ val pv: DynamicValue = DynamicValue.Primitive(PrimitiveValue.Int(42))
646
+ val ps: DynamicValue = DynamicValue.Primitive(PrimitiveValue.String("hello"))
647
+ ```
648
+
649
+ The `PrimitiveValue` case names (`Int`, `Long`, `String`, `Boolean`, `Double`, etc.) match the Scala primitive names.
650
+
651
+ ### Converting Between Typed Values and DynamicValue
652
+
653
+ ```scala
654
+ // ZIO Schema 1.x
655
+ // toDynamic is a method on Schema[A], not on the value itself
656
+ val dv: DynamicValue = Person.schema.toDynamic(person)
657
+ // toTypedValue requires an implicit Schema[Person] in scope
658
+ val back: Either[String, Person] = dv.toTypedValue[Person]
659
+
660
+ // ZIO Blocks Schema
661
+ val dv: DynamicValue = Person.schema.toDynamicValue(person)
662
+ val back: Either[SchemaError, Person] = Person.schema.fromDynamicValue(dv)
663
+ ```
664
+
665
+ Two things change: `toDynamic` is renamed `toDynamicValue` (still on `Schema[A]`), and `toTypedValue` is replaced by `schema.fromDynamicValue`. The error type changes from `String` to `SchemaError`.
666
+
667
+ ### DynamicValue Operations
668
+
669
+ ZIO Blocks `DynamicValue` has a rich operation API that was absent in ZIO Schema. Where ZIO Schema required you to convert back to a typed value to manipulate data, you can now operate directly on `DynamicValue`:
670
+
671
+ ```scala
672
+ import zio.blocks.schema._
673
+ import zio.blocks.chunk.Chunk
674
+
675
+ val record = DynamicValue.Record(
676
+ Chunk(
677
+ "name" -> DynamicValue.Primitive(PrimitiveValue.String("Alice")),
678
+ "age" -> DynamicValue.Primitive(PrimitiveValue.Int(30))
679
+ )
680
+ )
681
+
682
+ // Navigate — get(fieldName) returns DynamicValueSelection (supports chaining)
683
+ // Call .one to extract a single value as Either[SchemaError, DynamicValue]
684
+ val name: Either[SchemaError, DynamicValue] = record.get("name").one
685
+
686
+ // Modify — set returns DynamicValue directly (silent no-op if path not found)
687
+ // Use setOrFail to get an Either on missing paths
688
+ val updated: DynamicValue = record.set(
689
+ DynamicOptic.root.field("name"),
690
+ DynamicValue.Primitive(PrimitiveValue.String("Bob"))
691
+ )
692
+
693
+ // Diff
694
+ val other = DynamicValue.Record(Chunk(
695
+ "name" -> DynamicValue.Primitive(PrimitiveValue.String("Bob")),
696
+ "age" -> DynamicValue.Primitive(PrimitiveValue.Int(31))
697
+ ))
698
+ val patch = record.diff(other)
699
+ ```
700
+
701
+ ---
702
+
703
+ ## Migrating Schema Introspection
704
+
705
+ ### MetaSchema → DynamicSchema
706
+
707
+ ZIO Schema has `MetaSchema` (a type-erased structural description of a schema) and `schema.ast` to convert to it. ZIO Blocks uses `DynamicSchema` for the same purpose:
708
+
709
+ ```scala
710
+ // ZIO Schema 1.x
711
+ val meta: MetaSchema = schema.ast
712
+ val back: Schema[_] = meta.toSchema
713
+
714
+ // ZIO Blocks Schema
715
+ val dynamic: DynamicSchema = schema.toDynamicSchema
716
+ ```
717
+
718
+ `DynamicSchema` wraps a `Reflect[NoBinding, _]` — the full structural description without runtime constructors or deconstructors. Use it for:
719
+
720
+ - Runtime structural validation of `DynamicValue` instances
721
+ - Dynamic schema loading from configuration or network
722
+ - Schema inspection without compile-time type information
723
+
724
+ ```scala
725
+ // ZIO Blocks Schema — validate a DynamicValue against a schema
726
+ val personSchema: Schema[Person] = Schema.derived[Person]
727
+ val dynSchema: DynamicSchema = personSchema.toDynamicSchema
728
+
729
+ val value = DynamicValue.Record(Chunk(
730
+ "name" -> DynamicValue.Primitive(PrimitiveValue.String("Alice")),
731
+ "age" -> DynamicValue.Primitive(PrimitiveValue.Int(30))
732
+ ))
733
+
734
+ dynSchema.conforms(value) // true
735
+ dynSchema.check(value) // None (no error)
736
+ ```
737
+
738
+ :::warning
739
+ ZIO Schema's `Migration` system for schema-to-schema migration (i.e., automatically migrating values from one version of a type to another) is **not yet available** in ZIO Blocks Schema. The `schema.migrate[B](newSchema)` and `schema.coerce[B](newSchema)` methods do not exist. If your application relies on schema migration, you have two options:
740
+
741
+ 1. Implement migration logic manually using `DynamicValue` transformations and `DynamicSchema` for validation.
742
+ 2. Wait for schema migration support to be added to ZIO Blocks Schema (it is on the roadmap).
743
+ :::
744
+
745
+ ### Schema Serialization
746
+
747
+ ZIO Schema supports serializing a schema itself (via `schema.serializable`). ZIO Blocks Schema does not have a direct equivalent at this time. All schema metadata types (`DynamicOptic`, `DynamicPatch`, `Modifier`, `Validation`, etc.) have `Schema` instances and are individually serializable, but there is no single `Schema[Schema[A]]` that round-trips the full structural description.
748
+
749
+ ---
750
+
751
+ ## Migrating Optics
752
+
753
+ ZIO Schema uses an `AccessorBuilder` pattern that delegates optic creation to an external `zio-schema-optics` module. ZIO Blocks Schema includes a complete, first-class optics system in the core module.
754
+
755
+ ### Generating Optics
756
+
757
+ **Before (ZIO Schema 1.x with `zio-schema-optics`):**
758
+
759
+ ```scala
760
+ import zio.schema._
761
+ import zio.schema.optics._
762
+
763
+ case class Person(name: String, age: Int)
764
+ object Person {
765
+ implicit val schema: Schema[Person] = DeriveSchema.gen
766
+ val (name, age) = schema.makeAccessors(ZioOpticsBuilder)
767
+ }
768
+ ```
769
+
770
+ **After (ZIO Blocks Schema):**
771
+
772
+ Optics are generated by the macro derivation and placed directly in the companion object as `Lens` instances via a `CompanionOptics` mechanism. In Scala 3, they are generated automatically. In Scala 2, use `Schema.derived[Person]` and access fields by calling `schema.reflect.asRecord.get.lensByName[String]("name")`, or use the macro-derived companion optics pattern:
773
+
774
+ ```scala
775
+ // Scala 3 — optics generated in companion via macro
776
+ import zio.blocks.schema._
777
+
778
+ case class Person(name: String, age: Int)
779
+ object Person extends CompanionOptics[Person] {
780
+ implicit val schema: Schema[Person] = Schema.derived[Person]
781
+ // Scala 3 macro generates: val name: Lens[Person, String] = ...
782
+ // val age: Lens[Person, Int] = ...
783
+ }
784
+
785
+ // Usage
786
+ val lens: Lens[Person, String] = Person.name
787
+ val person = Person("Alice", 30)
788
+ lens.modify(person, _.toUpperCase) // Person("ALICE", 30)
789
+ ```
790
+
791
+ ```scala
792
+ // Scala 2 — obtain lenses from the schema
793
+ import zio.blocks.schema._
794
+
795
+ case class Person(name: String, age: Int)
796
+ object Person {
797
+ implicit val schema: Schema[Person] = Schema.derived[Person]
798
+
799
+ val name: Lens[Person, String] =
800
+ schema.reflect.asRecord.get.lensByName[String]("name").get
801
+ val age: Lens[Person, Int] =
802
+ schema.reflect.asRecord.get.lensByName[Int]("age").get
803
+ }
804
+ ```
805
+
806
+ ### Using Optics
807
+
808
+ The four optic types in ZIO Blocks are `Lens`, `Prism`, `Optional`, and `Traversal`. Their usage API is similar to standard optics libraries:
809
+
810
+ ```scala
811
+ import zio.blocks.schema._
812
+
813
+ case class Person(name: String, age: Int)
814
+ object Person {
815
+ implicit val schema: Schema[Person] = Schema.derived[Person]
816
+ }
817
+
818
+ // Obtain lens (Scala 2 example)
819
+ val nameLens: Lens[Person, String] =
820
+ Person.schema.reflect.asRecord.get.lensByName[String]("name").get
821
+
822
+ val person = Person("Alice", 30)
823
+
824
+ // Get
825
+ val name: String = nameLens.get(person) // "Alice"
826
+
827
+ // Modify
828
+ val upper: Person = nameLens.modify(person, _.toUpperCase) // Person("ALICE", 30)
829
+
830
+ // Replace — note: ZIO Blocks uses replace, not set (unlike Monocle and many other optics libraries)
831
+ val renamed: Person = nameLens.replace(person, "Bob") // Person("Bob", 30)
832
+ ```
833
+
834
+ For sealed traits, use `Prism`:
835
+
836
+ ```scala
837
+ import zio.blocks.schema._
838
+
839
+ sealed trait Shape
840
+ case class Circle(radius: Double) extends Shape
841
+ case class Rectangle(w: Double, h: Double) extends Shape
842
+
843
+ object Shape {
844
+ implicit val schema: Schema[Shape] = Schema.derived[Shape]
845
+
846
+ val circlePrism: Prism[Shape, Circle] =
847
+ schema.reflect.asVariant.get.prismByName[Circle]("Circle").get
848
+ }
849
+
850
+ val shape: Shape = Circle(5.0)
851
+ Shape.circlePrism.getOption(shape) // Some(Circle(5.0))
852
+ Shape.circlePrism.reverseGet(Circle(3.0)) // Circle(3.0): Shape
853
+ ```
854
+
855
+ ### Schema Expressions (New in ZIO Blocks)
856
+
857
+ ZIO Blocks introduces `SchemaExpr[S, A]`, a typed expression language built on top of optics. There is no equivalent in ZIO Schema. These allow you to build inspectable, composable predicates and computations:
858
+
859
+ ```scala
860
+ import zio.blocks.schema._
861
+
862
+ case class Product(name: String, price: Double, inStock: Boolean)
863
+ object Product {
864
+ implicit val schema: Schema[Product] = Schema.derived[Product]
865
+ val priceLens: Lens[Product, Double] =
866
+ schema.reflect.asRecord.get.lensByName[Double]("price").get
867
+ val inStockLens: Lens[Product, Boolean] =
868
+ schema.reflect.asRecord.get.lensByName[Boolean]("inStock").get
869
+ }
870
+
871
+ // Build a typed predicate expression
872
+ val cheapAndInStock: SchemaExpr[Product, Boolean] =
873
+ (Product.priceLens < 100.0) && (Product.inStockLens === true)
874
+
875
+ // Evaluate against data — eval returns Either[OpticCheck, Seq[A]]
876
+ // Right(Seq(true)) on success
877
+ // Left(OpticCheck) if a prism in the path did not match
878
+ val p = Product("Widget", 49.99, inStock = true)
879
+ cheapAndInStock.eval(p) // Right(Seq(true))
880
+ ```
881
+
882
+ ---
883
+
884
+ ## Migrating Diff and Patch
885
+
886
+ ### Diff
887
+
888
+ ZIO Schema uses `Differ.fromSchema(schema).diff(a, b)` or the convenience method `schema.diff(a, b)`. ZIO Blocks Schema uses the same convenience method:
889
+
890
+ ```scala
891
+ // ZIO Schema 1.x
892
+ val patch: Patch[Person] = Person.schema.diff(person1, person2)
893
+
894
+ // ZIO Blocks Schema
895
+ val patch: Patch[Person] = Person.schema.diff(person1, person2)
896
+ ```
897
+
898
+ The call site is identical, but the underlying `Patch` types are different.
899
+
900
+ ### Patch Application
901
+
902
+ ```scala
903
+ // ZIO Schema 1.x
904
+ val result: Either[String, Person] = Person.schema.patch(person, patch)
905
+
906
+ // ZIO Blocks Schema
907
+ val result: Either[SchemaError, Person] = Person.schema.patch(person, patch)
908
+ // or equivalently:
909
+ val result: Either[SchemaError, Person] = patch.apply(person, PatchMode.Strict)
910
+ ```
911
+
912
+ The error type changes from `String` to `SchemaError`.
913
+
914
+ ### Creating Patches Programmatically
915
+
916
+ ZIO Schema has no structured API for creating patches programmatically. ZIO Blocks Schema provides one through `Patch` smart constructors:
917
+
918
+ ```scala
919
+ import zio.blocks.schema._
920
+ import zio.blocks.schema.patch._
921
+
922
+ case class Person(name: String, age: Int)
923
+ object Person {
924
+ implicit val schema: Schema[Person] = Schema.derived[Person]
925
+ val nameLens: Lens[Person, String] =
926
+ schema.reflect.asRecord.get.lensByName[String]("name").get
927
+ val ageLens: Lens[Person, Int] =
928
+ schema.reflect.asRecord.get.lensByName[Int]("age").get
929
+ }
930
+
931
+ // Set a field
932
+ val renamePatch: Patch[Person] = Patch.set(Person.nameLens, "Bob")
933
+
934
+ // Compose patches
935
+ val combined: Patch[Person] = renamePatch ++ Patch.set(Person.ageLens, 31)
936
+
937
+ // Apply
938
+ val updated: Either[SchemaError, Person] = combined(Person("Alice", 30), PatchMode.Strict)
939
+ ```
940
+
941
+ ---
942
+
943
+ ## Migrating Type Class Derivation
944
+
945
+ ### Before (ZIO Schema 1.x)
946
+
947
+ ZIO Schema does not have a general `Deriver[TC]` interface. Each codec module implements its own derivation logic independently. There is no way to derive an arbitrary user-defined type class from a `Schema[A]`.
948
+
949
+ ### After (ZIO Blocks Schema)
950
+
951
+ ZIO Blocks Schema introduces `Deriver[TC]`, a unified interface for deriving any type class `TC[_]` from a schema. This replaces ad-hoc codec-specific derivation:
952
+
953
+ ```scala
954
+ import zio.blocks.schema._
955
+ import zio.blocks.schema.binding._
956
+ import zio.blocks.schema.derive.Deriver
957
+ import zio.blocks.docs.Doc
958
+ import zio.blocks.typeid.TypeId
959
+
960
+ // Define a type class
961
+ trait Show[A] {
962
+ def show(a: A): String
963
+ }
964
+
965
+ // Implement Deriver[Show]
966
+ object DeriveShow extends Deriver[Show] {
967
+
968
+ def derivePrimitive[A](
969
+ primitiveType: PrimitiveType[A],
970
+ typeId: TypeId[A],
971
+ binding: Binding[BindingType.Primitive, A],
972
+ doc: Doc,
973
+ modifiers: Seq[Modifier.Reflect],
974
+ defaultValue: Option[A],
975
+ examples: Seq[A]
976
+ ): Lazy[Show[A]] = Lazy {
977
+ new Show[A] {
978
+ def show(a: A): String = a.toString
979
+ }
980
+ }
981
+
982
+ def deriveRecord[F[_, _], A](
983
+ fields: IndexedSeq[Term[F, A, _]],
984
+ typeId: TypeId[A],
985
+ binding: Binding[BindingType.Record, A],
986
+ doc: Doc,
987
+ modifiers: Seq[Modifier.Reflect],
988
+ defaultValue: Option[A],
989
+ examples: Seq[A]
990
+ )(implicit F: HasBinding[F], D: DeriveShow.HasInstance[F]): Lazy[Show[A]] = {
991
+ val recordBinding = binding.asInstanceOf[Binding.Record[A]]
992
+ val recordFields = fields.asInstanceOf[IndexedSeq[Term[Binding, A, _]]]
993
+ val recordReflect = new Reflect.Record[Binding, A](recordFields, typeId, recordBinding, doc, modifiers)
994
+ Lazy {
995
+ new Show[A] {
996
+ private lazy val resolvedShows: IndexedSeq[Show[Any]] =
997
+ fields.map(f => D.instance(f.value.metadata).asInstanceOf[Lazy[Show[Any]]].force)
998
+ def show(a: A): String = {
999
+ val regs = Registers(recordReflect.usedRegisters)
1000
+ recordBinding.deconstructor.deconstruct(regs, RegisterOffset.Zero, a)
1001
+ val fieldStrs = fields.indices.map { i =>
1002
+ val v = recordReflect.registers(i).get(regs, RegisterOffset.Zero)
1003
+ s"${fields(i).name} = ${resolvedShows(i).show(v)}"
1004
+ }
1005
+ s"${typeId.name}(${fieldStrs.mkString(", ")})"
1006
+ }
1007
+ }
1008
+ }
1009
+ }
1010
+
1011
+ // ... deriveVariant, deriveSequence, deriveMap, deriveDynamic, deriveWrapper
1012
+ // (see the DeriveShowExample in the examples module for full implementation)
1013
+ }
1014
+
1015
+ // Derive Show for any type
1016
+ case class Person(name: String, age: Int)
1017
+ object Person {
1018
+ implicit val schema: Schema[Person] = Schema.derived[Person]
1019
+ implicit val show: Show[Person] = schema.derive(DeriveShow)
1020
+ }
1021
+
1022
+ Person.show.show(Person("Alice", 30)) // Person(name = "Alice", age = 30)
1023
+ ```
1024
+
1025
+ ---
1026
+
1027
+ ## Migrating Validation
1028
+
1029
+ ### Before (ZIO Schema 1.x)
1030
+
1031
+ ZIO Schema uses a composable `Validation[A]` ADT as an annotation, attached via `@validate(...)` or `.validation(...)`:
1032
+
1033
+ ```scala
1034
+ import zio.schema._
1035
+ import zio.schema.validation._
1036
+ import zio.schema.annotation._
1037
+
1038
+ case class User(
1039
+ @validate(Validation.greaterThan(0)) age: Int,
1040
+ @validate(Validation.minLength(3)) name: String
1041
+ )
1042
+ object User {
1043
+ implicit val schema: Schema[User] = DeriveSchema.gen
1044
+ }
1045
+
1046
+ schema.validate(User(-1, "Al"))
1047
+ // Returns Chunk[ValidationError] with violations
1048
+ ```
1049
+
1050
+ ### After (ZIO Blocks Schema)
1051
+
1052
+ ZIO Blocks Schema has a simpler, non-composable `Validation[A]` that is embedded inside `PrimitiveType[A]` and checked during `DynamicSchema.check`. It is not composable with `And`/`Or`/`Not`:
1053
+
1054
+ ```scala
1055
+ import zio.blocks.schema._
1056
+
1057
+ // Validation is checked during DynamicSchema.check — not during fromDynamicValue
1058
+ val dynSchema = Schema[Int].toDynamicSchema
1059
+
1060
+ val valid = DynamicValue.Primitive(PrimitiveValue.Int(5))
1061
+ val invalid = DynamicValue.Primitive(PrimitiveValue.Int(-1))
1062
+
1063
+ dynSchema.conforms(valid) // true
1064
+ dynSchema.conforms(invalid) // true (no validation constraint on the base Int schema)
1065
+ ```
1066
+
1067
+ For validated types, use `Schema[A].transform` with a throwing `to` function, which signals failure during `fromDynamicValue`:
1068
+
1069
+ ```scala
1070
+ import zio.blocks.schema._
1071
+
1072
+ // Validated positive integer
1073
+ val positiveIntSchema: Schema[Int] =
1074
+ Schema[Int].transform(
1075
+ to = n => if (n > 0) n else throw SchemaError.validationFailed("Must be positive"),
1076
+ from = identity
1077
+ )
1078
+
1079
+ positiveIntSchema.fromDynamicValue(
1080
+ DynamicValue.Primitive(PrimitiveValue.Int(-1))
1081
+ )
1082
+ // Left(SchemaError: Must be positive)
1083
+ ```
1084
+
1085
+ For struct-level validation across multiple fields, implement validation in the `to` function of a wrapper:
1086
+
1087
+ ```scala
1088
+ import zio.blocks.schema._
1089
+
1090
+ final case class AgeRange(min: Int, max: Int)
1091
+ object AgeRange {
1092
+ implicit val schema: Schema[AgeRange] = Schema.derived[AgeRange]
1093
+ // Schema-level validation is handled through the derived schema's
1094
+ // DynamicSchema.check, or by adding custom validation in a wrapping transform.
1095
+ }
1096
+ ```
1097
+
1098
+ :::info
1099
+ If you rely heavily on ZIO Schema's composable validation (chaining `And`, `Or`, `Not`, `Transform` validators), you will need to implement that logic in the `to` function of a `Schema.transform` wrapper, or in application-level validation code. ZIO Blocks Schema's built-in `Validation` is deliberately simpler: it covers the most common primitive constraints without the complexity of a full combinator library.
1100
+ :::
1101
+
1102
+ ---
1103
+
1104
+ ## Migrating the Fail Schema
1105
+
1106
+ ZIO Schema provides `Schema.fail[A](message: String)` to represent the absence of schema information:
1107
+
1108
+ ```scala
1109
+ // ZIO Schema 1.x
1110
+ val missing: Schema[MyType] = Schema.fail("No schema available for MyType")
1111
+ ```
1112
+
1113
+ ZIO Blocks Schema has no equivalent `Fail` schema node. The recommended approach is to leave the implicit schema undefined and let the compiler report the missing instance, or to throw from a type class derivation:
1114
+
1115
+ ```scala
1116
+ // ZIO Blocks Schema — no Schema.fail; use a compile error or a runtime exception approach
1117
+ // If you need a runtime sentinel, use Schema[DynamicValue] or create a minimal placeholder:
1118
+ val placeholder: Schema[DynamicValue] = Schema[DynamicValue]
1119
+ ```
1120
+
1121
+ ---
1122
+
1123
+ ## Summary of Missing Features
1124
+
1125
+ The following ZIO Schema features do not yet have equivalents in ZIO Blocks Schema:
1126
+
1127
+ | Feature | Status |
1128
+ |---|---|
1129
+ | `Schema.fail` / fail schemas | Not available |
1130
+ | `Schema.migrate[B]` / `Schema.coerce[B]` | Not available — schema migration is planned |
1131
+ | `MetaSchema` / schema serialization | Partial — `DynamicSchema` covers structural inspection; full schema round-trip is not available |
1132
+ | `Fallback[A, B]` schema | Not available |
1133
+ | `NonEmptyChunk` / `NonEmptyMap` schemas | Not available — use wrapper types |
1134
+ | `Schema.Singleton` / singleton schemas | Not available |
1135
+ | `DynamicValue.BothValue` / `DynamicValue.DynamicAst` | Not available |
1136
+ | Composable `Validation` (`And`, `Or`, `Not`) | Not available — use `transform` with throwing functions |
1137
+ | Streaming codec methods (`streamEncoder`, `streamDecoder`) | Not available — wrap codecs in your effect system |
1138
+ | ZIO `Chunk` (from `zio-core`) | Replaced by `zio.blocks.chunk.Chunk` |
1139
+
1140
+ ---
1141
+
1142
+ ## Running the Examples
1143
+
1144
+ All code from this guide is available as runnable examples in the `schema-examples` module.
1145
+
1146
+ **1. Clone the repository and navigate to the project:**
1147
+
1148
+ ```bash
1149
+ git clone https://github.com/zio/zio-blocks.git
1150
+ cd zio-blocks
1151
+ ```
1152
+
1153
+ **2. Run individual examples with sbt:**
1154
+
1155
+ ```bash
1156
+ # Step 1: Schema derivation, primitives, and DynamicValue roundtrip
1157
+ sbt "schema-examples/runMain ziosschemamigration.Step1SchemaDerivedAndPrimitives"
1158
+
1159
+ # Step 2: Modifiers and transform (annotations, newtypes)
1160
+ sbt "schema-examples/runMain ziosschemamigration.Step2ModifiersAndTransform"
1161
+
1162
+ # Step 3: Optics (Lens, Prism) and DynamicSchema validation
1163
+ sbt "schema-examples/runMain ziosschemamigration.Step3OpticsAndDynamicSchema"
1164
+
1165
+ # Step 4: Diff and patch
1166
+ sbt "schema-examples/runMain ziosschemamigration.Step4DiffAndPatch"
1167
+
1168
+ # Complete example: end-to-end e-commerce domain
1169
+ sbt "schema-examples/runMain ziosschemamigration.CompleteMigrationExample"
1170
+
1171
+ # Type class derivation — deriving Show from a Schema
1172
+ sbt "schema-examples/runMain typeclassderivation.DeriveShowExample"
1173
+
1174
+ # Type class derivation — deriving a random generator from a Schema
1175
+ sbt "schema-examples/runMain typeclassderivation.DeriveGenExample"
1176
+ ```
1177
+
1178
+ **3. Or compile all examples at once:**
1179
+
1180
+ ```bash
1181
+ sbt "schema-examples/compile"
1182
+ ```
1183
+
1184
+ ---
1185
+
1186
+ ## Going Further
1187
+
1188
+ - [Schema Reference](../reference/schema.md) — full `Schema[A]` API
1189
+ - [Reflect Reference](../reference/reflect.md) — the `Reflect[F, A]` node types
1190
+ - [Binding Reference](../reference/binding.md) — constructors, deconstructors, and the register system
1191
+ - [Optics Reference](../reference/optics.md) — `Lens`, `Prism`, `Optional`, `Traversal`
1192
+ - [Type Class Derivation Guide](../reference/type-class-derivation.md) — implementing `Deriver[TC]`
1193
+ - [Codec Reference](../reference/codec.md) — the `Format` and `Codec` infrastructure
1194
+ - [DynamicValue Reference](../reference/dynamic-value.md) — the `DynamicValue` API
1195
+ - [Validation Reference](../reference/validation.md) — built-in validation constraints