@zio.dev/zio-blocks 0.0.1

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,619 @@
1
+ ---
2
+ id: schema
3
+ title: "Schema"
4
+ ---
5
+
6
+ `Schema[A]` is the primary data type in ZIO Blocks (ZIO Schema 2) that contains reified information about the structure of a Scala data type `A`, together with the ability to tear down and build up values of that type.
7
+
8
+ ```scala
9
+ final case class Schema[A](reflect: Reflect.Bound[A])
10
+ ```
11
+
12
+ ```
13
+ ┌────────────────────────────────────────────────────────────────┐
14
+ │ Schema[A] │
15
+ ├────────────────────────────────────────────────────────────────┤
16
+ │ Reflect.Bound[A] (Reflect[Binding, A]) │
17
+ │ ┌─────────────────────────────────────────────────────┐ │
18
+ │ │ Structure │ TypeName[A], DynamicValue │ │
19
+ │ │ (ADT nodes) │ Doc, Examples, Default Value │ │
20
+ │ │ │ Modifiers, Metadata │ │
21
+ │ └─────────────────────────────────────────────────────┘ │
22
+ │ ┌─────────────────────────────────────────────────────┐ │
23
+ │ │ Binding[T, A] │ │
24
+ │ │ - Constructor[A] (build values) │ │
25
+ │ │ - Deconstructor[A] (tear down values) │ │
26
+ │ └─────────────────────────────────────────────────────┘ │
27
+ └────────────────────────────────────────────────────────────────┘
28
+ ```
29
+
30
+ ## Reflect: Structure vs Binding
31
+
32
+ The [`Reflect`](./reflect.md) data type represents the structure of Scala types. It is parameterized by `F[_, _]` which allows plugging in different "binding" strategies:
33
+
34
+ ```
35
+ ┌────────────────────────────────────────────────────────────────┐
36
+ │ Reflect[F[_, _], A] │
37
+ ├────────────────────────────────────────────────────────────────┤
38
+ │ F = Binding → Reflect.Bound[A] (with construction/ │
39
+ │ deconstruction) │
40
+ │ F = NoBinding → Reflect.Unbound[A] (pure data, │
41
+ │ serializable) │
42
+ └────────────────────────────────────────────────────────────────┘
43
+ ```
44
+
45
+ - **Bound Schema**: Contains functions for constructing/deconstructing values (not serializable)
46
+ - **Unbound Schema**: Pure data representation (can be serialized across the wire)
47
+
48
+ Schema is a bound Reflect, meaning that other than structural information, it also contains construction and deconstruction capabilities via the `Binding` type:
49
+
50
+ ```scala
51
+ // Schema wraps a bound Reflect
52
+ final case class Schema[A](reflect: Reflect.Bound[A])
53
+
54
+ // Reflect.Bound is a type alias
55
+ type Bound[A] = Reflect[Binding, A]
56
+ ```
57
+
58
+ ## Schema Derivation
59
+
60
+ ZIO Blocks provides both automatic schema derivation and also ways to manually create schemas. As an end user, you will mostly use automatic derivation. So we will focus on automatic derivation here. If you need to go deeper and create schemas manually, check out the [Reflect](./reflect.md) and [Binding](./binding.md) documentation pages.
61
+
62
+ To leverage auto-derivation, simply define an implicit `Schema` for your type using `Schema.derived`:
63
+
64
+ ```scala mdoc:compile-only
65
+ import zio.blocks.schema.Schema
66
+
67
+ case class Person(name: String, age: Int)
68
+
69
+ object Person {
70
+ implicit val schema: Schema[Person] = Schema.derived
71
+ }
72
+ ```
73
+
74
+ It will automatically derive the schema for `Person` based on its structure. After that, you can summon the schema using `Schema[Person]` anywhere in your code.
75
+
76
+ For sealed traits (ADTs), it will derive the schema for all subtypes as well:
77
+
78
+ ```scala mdoc:compile-only
79
+ import zio.blocks.schema.Schema
80
+
81
+ sealed trait Shape
82
+ object Shape {
83
+ case class Circle(radius: Double) extends Shape
84
+ case class Rectangle(width: Double, height: Double) extends Shape
85
+
86
+ implicit val schema: Schema[Shape] = Schema.derived
87
+ }
88
+ ```
89
+
90
+ Under the hood, the derivation process includes deriving schemas for cases of enum (i.e. `Circle` and `Rectangle`) and finally creating a schema for the `Shape` trait itself.
91
+
92
+ ## Built-in Schemas
93
+
94
+ ZIO Blocks ships with a comprehensive set of pre-defined schemas for standard Scala and Java types, eliminating boilerplate and ensuring consistent behavior across your application.
95
+
96
+ ### Primitive Types
97
+
98
+ The foundational building blocks for all data types. These schemas leverage the [register-based architecture](registers.md) for zero-allocation performance:
99
+
100
+ ```scala mdoc:compile-only
101
+ import zio.blocks.schema.Schema
102
+
103
+ Schema[Unit] // The unit type (singleton value)
104
+ Schema[Boolean] // Boolean values (true/false)
105
+ Schema[Byte] // 8-bit signed integer (-128 to 127)
106
+ Schema[Short] // 16-bit signed integer
107
+ Schema[Int] // 32-bit signed integer
108
+ Schema[Long] // 64-bit signed integer
109
+ Schema[Float] // 32-bit IEEE 754 floating point
110
+ Schema[Double] // 64-bit IEEE 754 floating point
111
+ Schema[Char] // 16-bit Unicode character
112
+ Schema[String] // Immutable character sequence
113
+ ```
114
+
115
+ ### Arbitrary Precision Numbers
116
+
117
+ For financial calculations and scenarios requiring exact decimal representation:
118
+
119
+ ```scala mdoc:compile-only
120
+ import zio.blocks.schema.Schema
121
+
122
+ Schema[BigInt] // Arbitrary precision integer
123
+ Schema[BigDecimal] // Arbitrary precision decimal
124
+ ```
125
+
126
+ ### Temporal Types (java.time)
127
+
128
+ Complete coverage of the modern Java Time API for robust date/time handling:
129
+
130
+ ```scala mdoc:compile-only
131
+ import zio.blocks.schema.Schema
132
+
133
+ // Date components
134
+ Schema[java.time.LocalDate] // Date without time (2024-01-15)
135
+ Schema[java.time.LocalTime] // Time without date (14:30:00)
136
+ Schema[java.time.LocalDateTime] // Date and time without timezone
137
+
138
+ // Timezone-aware types
139
+ Schema[java.time.ZonedDateTime] // Full date-time with timezone
140
+ Schema[java.time.OffsetDateTime] // Date-time with UTC offset
141
+ Schema[java.time.OffsetTime] // Time with UTC offset
142
+ Schema[java.time.ZoneId] // Timezone identifier (e.g., "America/New_York")
143
+ Schema[java.time.ZoneOffset] // Fixed UTC offset (e.g., +05:30)
144
+
145
+ // Duration and period
146
+ Schema[java.time.Duration] // Time-based duration (hours, minutes, seconds)
147
+ Schema[java.time.Period] // Date-based period (years, months, days)
148
+ Schema[java.time.Instant] // Point on the timeline (Unix timestamp)
149
+
150
+ // Calendar components
151
+ Schema[java.time.Year] // Year value (2024)
152
+ Schema[java.time.Month] // Month of year (JANUARY..DECEMBER)
153
+ Schema[java.time.DayOfWeek] // Day of week (MONDAY..SUNDAY)
154
+ Schema[java.time.YearMonth] // Year and month combination
155
+ Schema[java.time.MonthDay] // Month and day combination
156
+ ```
157
+
158
+ ### Common Java Utility Types
159
+
160
+ There are also schemas for frequently used Java utility types, UUID and Currency:
161
+
162
+ ```scala mdoc:compile-only
163
+ import zio.blocks.schema.Schema
164
+
165
+ Schema[java.util.UUID] // 128-bit universally unique identifier
166
+ Schema[java.util.Currency] // ISO 4217 currency code
167
+ ```
168
+
169
+ ### Optional Values
170
+
171
+ ZIO Blocks provides specialized `Option` schemas optimized for primitive types. These avoid boxing overhead by storing primitive values directly in registers:
172
+
173
+ ```scala mdoc:compile-only
174
+ import zio.blocks.schema.Schema
175
+
176
+ import zio.blocks.schema.Schema
177
+
178
+ // Specialized primitive options (no boxing overhead)
179
+ Schema[Option[Boolean]] // Also: Byte, Short, Int, Long, Float, Double, Char, Unit
180
+ ```
181
+
182
+ Other than primitive types, ZIO Blocks uses a generic representation for `Option[A]` which works for all reference types:
183
+
184
+ ```scala
185
+ import zio.blocks.schema.Schema
186
+
187
+ // Reference type options (requires A <: AnyRef)
188
+ Schema[Option[A]] // Generic option for reference types
189
+ ```
190
+
191
+ ### Collection Types
192
+
193
+ ZIO Blocks also provides polymorphic schemas for standard Scala collections. You can summon schemas for collections of any element type `A` (and key/value types `K`/`V` for maps):
194
+
195
+ ```scala
196
+ Schema[List[A]] // Immutable singly-linked list
197
+ Schema[Vector[A]] // Immutable indexed sequence (efficient random access)
198
+ Schema[Set[A]] // Immutable set (unique elements)
199
+ Schema[Seq[A]] // General immutable sequence
200
+ Schema[IndexedSeq[A]] // Indexed sequence
201
+ Schema[Map[K, V]] // Immutable key-value mapping
202
+ ```
203
+
204
+ To learn how to create custom collection schemas, check out the [`Sequence`](./reflect.md#3-sequence) and [`Map`](./reflect.md#4-map) nodes on the documentation of [`Reflect`](./reflect.md) data type.
205
+
206
+ ### DynamicValue
207
+
208
+ ZIO Blocks includes a built-in schema for `DynamicValue`, a semi-structured data representation that serves as a superset of JSON:
209
+
210
+ ```scala mdoc:compile-only
211
+ import zio.blocks.schema._
212
+
213
+ val schema = Schema[DynamicValue] // Semi-structured data (superset of JSON)
214
+ ```
215
+
216
+ Having the schema for `DynamicValue` allows seamless encoding/decoding between `DynamicValue` and other formats, such as JSON, Avro, Protobuf, etc. It enables us to convert our type-safe data into a semi-structured representation and serialize it into any desired format.
217
+
218
+ ### DynamicValue toString (EJSON Format)
219
+
220
+ `DynamicValue` has a custom `toString` that produces EJSON (Extended JSON) format - a superset of JSON that handles non-string keys, tagged variants, and typed primitives:
221
+
222
+ ```scala
223
+ import zio.blocks.schema._
224
+
225
+ // Records have unquoted keys
226
+ val record = DynamicValue.Record(Vector(
227
+ "name" -> DynamicValue.Primitive(PrimitiveValue.String("Alice")),
228
+ "age" -> DynamicValue.Primitive(PrimitiveValue.Int(30))
229
+ ))
230
+ println(record)
231
+ // {
232
+ // name: "Alice",
233
+ // age: 30
234
+ // }
235
+
236
+ // Maps have quoted string keys
237
+ val map = DynamicValue.Map(Vector(
238
+ DynamicValue.Primitive(PrimitiveValue.String("key")) ->
239
+ DynamicValue.Primitive(PrimitiveValue.String("value"))
240
+ ))
241
+ println(map)
242
+ // {
243
+ // "key": "value"
244
+ // }
245
+
246
+ // Variants use @ metadata
247
+ val variant = DynamicValue.Variant("Some", DynamicValue.Record(Vector(
248
+ "value" -> DynamicValue.Primitive(PrimitiveValue.Int(42))
249
+ )))
250
+ println(variant)
251
+ // {
252
+ // value: 42
253
+ // } @ {tag: "Some"}
254
+
255
+ // Typed primitives use @ metadata
256
+ val instant = DynamicValue.Primitive(PrimitiveValue.Instant(java.time.Instant.now))
257
+ println(instant)
258
+ // 1705312800000 @ {type: "instant"}
259
+ ```
260
+
261
+ **Key EJSON properties:**
262
+ - **Records**: unquoted keys (`{ name: "John" }`)
263
+ - **Maps**: quoted string keys (`{ "name": "John" }`) or unquoted non-string keys (`{ 42: "value" }`)
264
+ - **Variants**: postfix `@ {tag: "CaseName"}`
265
+ - **Typed primitives**: postfix `@ {type: "instant"}` for types that would lose precision as JSON
266
+
267
+ ## Debug-Friendly toString
268
+
269
+ `Schema` has a custom `toString` that wraps the underlying `Reflect` output in a `Schema { ... }` block, providing a complete structural view of your data types:
270
+
271
+ ```scala
272
+ import zio.blocks.schema._
273
+
274
+ case class Person(name: String, age: Int)
275
+ object Person {
276
+ implicit val schema: Schema[Person] = Schema.derived
277
+ }
278
+
279
+ println(Schema[Person])
280
+ // Output:
281
+ // Schema {
282
+ // record Person {
283
+ // name: String
284
+ // age: Int
285
+ // }
286
+ // }
287
+ ```
288
+
289
+ For primitive schemas:
290
+
291
+ ```scala
292
+ println(Schema[Int])
293
+ // Output:
294
+ // Schema {
295
+ // Int
296
+ // }
297
+ ```
298
+
299
+ This format makes it easy to inspect complex nested schemas during debugging. See the [Reflect documentation](./reflect.md#debug-friendly-tostring) for details on the SDL format used for the inner structure.
300
+
301
+ ## Encoding and Decoding
302
+
303
+ The `Schema[A]` provides methods to encode and decode values of type `A` to/from various formats using the `Format` abstraction:
304
+
305
+ ```scala
306
+ case class Schema[A](reflect: Reflect.Bound[A]) {
307
+ def encode[F <: codec.Format](format: F)(output: format.EncodeOutput)(value: A): Unit = ???
308
+ def decode[F <: codec.Format](format: F)(decodeInput: format.DecodeInput): Either[SchemaError, A] = ???
309
+ }
310
+ ```
311
+
312
+ The `Format` is the base abstraction for serialization formats in ZIO Blocks, such as Avro, JSON, Protobuf, etc. Each format associates with a specific codec type class which defines the interface for encoding and decoding values and a deriver for deriving codecs for specific types.
313
+
314
+ ZIO Blocks has built-in support for several popular formats, currently `AvroFormat` and `JsonFormat`. You can also implement your own custom formats by extending the `Format` trait.
315
+
316
+ The following example demonstrates encoding and decoding a `Person` case class to/from JSON using the built-in `JsonFormat`:
317
+
318
+ ```scala mdoc:compile-only
319
+ import java.nio.ByteBuffer
320
+
321
+ import zio.blocks.schema._
322
+ import zio.blocks.schema.json.JsonFormat
323
+
324
+ case class Person(name: String, age: Int)
325
+
326
+ object Person {
327
+ implicit val schema: Schema[Person] = Schema.derived
328
+ }
329
+
330
+ object EncodeDecodeExample extends App {
331
+ val person = Person("John", 42)
332
+
333
+ // Encode to JSON
334
+ val encodedBuffer = {
335
+ val buffer = ByteBuffer.allocate(1024)
336
+ Schema[Person].encode(JsonFormat)(buffer)(person)
337
+ buffer.flip()
338
+ buffer
339
+ }
340
+
341
+ // Extract JSON string
342
+ val jsonString = new String(encodedBuffer.duplicate().array())
343
+ println(s"Encoded JSON: $jsonString")
344
+
345
+ // Decode back to Person
346
+ val decodedPerson = Schema[Person].decode(JsonFormat)(encodedBuffer.duplicate())
347
+ println(s"Decoded Person: $decodedPerson")
348
+ }
349
+ ```
350
+
351
+ Please note that both `Schema#encode` and `Schema#decode` cache instances, so using encode and decode in multiple places only performs the derivation process once.
352
+
353
+ ## Type Class Derivation
354
+
355
+ To derive type class instances for a type `A` based on its schema, you can use the `derive` method on `Schema[A]`. This method takes a `Deriver` for the desired type class and produces an instance of that type class for `A`.
356
+
357
+ In the following example, we derive a JSON codec for the `Person` case class using the `JsonFormat` deriver:
358
+
359
+ ```scala
360
+ import zio.blocks.schema.Schema
361
+ import zio.blocks.schema.json.{JsonFormat, JsonBinaryCodec}
362
+
363
+ case class Person(name: String, age: Int)
364
+
365
+ object Person {
366
+ implicit val schema: Schema[Person] = Schema.derived
367
+ val codec: JsonBinaryCodec[Person] = schema.derive(JsonFormat)
368
+ }
369
+
370
+ val person = Person("John", 42)
371
+
372
+ // Encode to JSON
373
+ val json = Person.codec.encode(person)
374
+
375
+ // Extract JSON string
376
+ val jsonString = new String(json)
377
+ println(s"Encoded JSON: $jsonString")
378
+
379
+ // Decode back to Person
380
+ val decodedPerson = Person.codec.decode(json)
381
+ println(s"Decoded Person: $decodedPerson")
382
+ ```
383
+
384
+ ## Metadata Operations
385
+
386
+ ZIO Blocks allows attaching metadata to schemas and their fields, such as documentation, example values, and default values. This metadata can be useful for generating API documentation, client code, or providing hints to serialization formats.
387
+
388
+ Here is an example of how to set and retrieve documentation values:
389
+
390
+ ```scala mdoc:silent
391
+ import zio.blocks.schema._
392
+
393
+ case class Person(name: String, age: Int)
394
+
395
+ object Person extends CompanionOptics[Person]{
396
+ implicit val schema: Schema[Person] = Schema.derived
397
+
398
+ val name: Lens[Person, String] = optic(_.name)
399
+ val age: Lens[Person, Int] = optic(_.age)
400
+ }
401
+
402
+ // Add documentation to the schema
403
+ val documented: Schema[Person] = Schema[Person].doc("A person entity")
404
+
405
+ // Get documentation
406
+ val doc: Doc = Schema[Person].doc
407
+
408
+ // Add documentation to a field using optics
409
+ val fieldDoc: Schema[Person] = Schema[Person].doc(Person.name, "The person's name")
410
+ ```
411
+
412
+ The important thing to note here is that we can use optics to target specific fields within the schema when adding or retrieving metadata. In the last example, we added documentation specifically to the `name` field of the `Person` schema.
413
+
414
+ Similarly, you can set and get example values:
415
+
416
+ ```scala mdoc:compile-only
417
+ import zio.blocks.schema._
418
+
419
+ // Add example values
420
+ val withExamples: Schema[Person] =
421
+ Schema[Person].examples(Person("Alice", 30), Person("Bob", 25))
422
+
423
+ // Get examples
424
+ val examples: Seq[Person] = Schema[Person].examples
425
+
426
+ // Add examples to a specific field
427
+ val fieldExamples: Schema[Person] =
428
+ Schema[Person].examples(Person.name, "Alice", "Bob", "Charlie")
429
+ ```
430
+
431
+ There are also methods for setting and getting default values:
432
+
433
+ ```scala mdoc:silent
434
+ // Add default value to schema
435
+ val withDefault: Schema[Person] = Schema[Person]
436
+ .defaultValue(Person("Unknown", 0))
437
+
438
+ // Get default value
439
+ val default: Option[Person] = Schema[Person].getDefaultValue
440
+
441
+ // Add default to a specific field
442
+ val fieldDefault: Schema[Person] = Schema[Person]
443
+ .defaultValue(Person.age, 18)
444
+
445
+ // Get default for a field
446
+ val ageDefault: Option[Int] =
447
+ Schema[Person].getDefaultValue(Person.age)
448
+ ```
449
+
450
+ ## Accessing and Updating Partial Schemas
451
+
452
+ To access a specific part of a schema, we can use the `Schema#get` method, which takes an optic and returns the reflection of the targeted field.
453
+
454
+ ```scala mdoc:silent:reset
455
+ import zio.blocks.schema._
456
+
457
+ case class Person(name: String, address: Address)
458
+
459
+ object Person extends CompanionOptics[Person]{
460
+ implicit val schema: Schema[Person] = Schema.derived
461
+
462
+ val name: Lens[Person, String] = optic(_.name)
463
+ val address: Lens[Person, Address] = optic(_.address)
464
+ }
465
+
466
+ case class Address(street: String, city: String)
467
+
468
+ object Address extends CompanionOptics[Address]{
469
+ implicit val schema: Schema[Address] = Schema.derived
470
+
471
+ val street: Lens[Address, String] = optic(_.street)
472
+ val city : Lens[Address, String] = optic(_.city)
473
+ }
474
+
475
+ // Get schema for a nested field
476
+ val addressSchema: Option[Reflect.Bound[Address]] =
477
+ Schema[Person].get(Person.address)
478
+ ```
479
+
480
+ This method enables us to retrieve the schema for any nested field using optics. For example, to get the schema for the `street` field inside the `address` of a `Person`, we can do as follows:
481
+
482
+ ```scala mdoc:compile-only
483
+ val streetSchema: Option[Reflect.Bound[String]] =
484
+ Schema[Person].get(Person.address(Address.street))
485
+ ```
486
+
487
+ ### Updating Nested Schemas
488
+
489
+ To update a specific part of a schema, we can use the `Schema#updated` method, which takes an optic and a function to transform the targeted schema:
490
+
491
+ ```scala
492
+ case class Schema[A](reflect: Reflect.Bound[A]) {
493
+ def updated[B](optic: Optic[A, B])(
494
+ f: Reflect.Bound[B] => Reflect.Bound[B]
495
+ ): Option[Schema[A]]
496
+ }
497
+ ```
498
+
499
+ Here is an example of updating the documentation of a nested field:
500
+
501
+ ```scala mdoc:compile-only
502
+ // Update schema at a specific path
503
+ val updated: Option[Schema[Person]] = Schema[Person]
504
+ .updated(Person.address)(_.doc("Mailing address"))
505
+ ```
506
+
507
+ ## Schema Aspects
508
+
509
+ Schema aspects are a powerful mechanism in ZIO Blocks for transforming schemas. You can think of the schema aspect as a function that takes a reflect and produces a new reflect:
510
+
511
+ ```scala
512
+ trait SchemaAspect[-Upper, +Lower, F[_, _]] {
513
+ def apply[A >: Lower <: Upper](reflect: Reflect[F, A]): Reflect[F, A]
514
+ def recursive(implicit ev1: Any <:< Upper, ev2: Lower <:< Nothing): SchemaAspect[Upper, Lower, F]
515
+ }
516
+ ```
517
+
518
+ The `Schema` data type has a `@@` method used for applying schema aspects:
519
+
520
+ ```scala
521
+ case class Schema[A](reflect: Reflect.Bound[A]) {
522
+ def @@[Min >: A, Max <: A](aspect: SchemaAspect[Min, Max, Binding]): Schema[A] = ???
523
+ def @@[B](part: Optic[A, B], aspect: SchemaAspect[B, B, Binding]) = ???
524
+ }
525
+ ```
526
+
527
+ These methods enable us to use `@@` syntax for applying aspects to either the entire schema or a specific path within the schema using optics:
528
+
529
+ ```scala mdoc:compile-only
530
+ import zio.blocks.schema._
531
+
532
+ // Apply aspect to entire schema
533
+ val documented: Schema[Person] = Schema[Person] @@ SchemaAspect.doc("A person")
534
+
535
+ // Apply aspect to specific path
536
+ val fieldDoc: Schema[Person] = Schema[Person] @@ (
537
+ Person.name,
538
+ SchemaAspect.examples("Alice", "Bob")
539
+ )
540
+ ```
541
+
542
+ Currently, ZIO Blocks provides the following built-in schema aspects:
543
+
544
+ - `SchemaAspect.identity`: No-op transformation
545
+ - `SchemaAspect.doc`: Attach documentation to schema or field
546
+ - `SchemaAspect.examples`: Attach example values to schema or field
547
+
548
+ ## Modifiers
549
+
550
+ Modifiers in ZIO Blocks provide a mechanism to attach metadata and configuration to schema elements without polluting the domain types themselves. They serve as the successor to ZIO Schema 1's annotation system, with the critical advantage of being **pure data** so, unlike Scala annotations, modifiers are runtime values that can be serialized.
551
+
552
+ The `Schema.modifier` and `Schema.modifiers` methods allow adding one or more modifiers to a schema:
553
+
554
+ ```scala
555
+ final case class Schema[A](reflect: Reflect.Bound[A]) {
556
+ def modifier(modifier: Modifier.Reflect) : Schema[A] = ???
557
+ def modifiers(modifiers: Iterable[Modifier.Reflect]): Schema[A] = ???
558
+ }
559
+ ```
560
+
561
+ Here is an example of adding modifiers to a schema:
562
+
563
+ ```scala mdoc:compile-only
564
+ // Add modifier to schema
565
+ val modified: Schema[Person] = Schema[Person]
566
+ .modifier(Modifier.config("db.table-name", "person_table"))
567
+ .modifier(Modifier.config("schema.version", "v2"))
568
+
569
+ // Add multiple modifiers
570
+ val multiModified: Schema[Person] = Schema[Person]
571
+ .modifiers(
572
+ Seq(
573
+ Modifier.config("db.table-name", "person_table"),
574
+ Modifier.config("schema.version", "v2")
575
+ )
576
+ )
577
+ ```
578
+
579
+ ## Wrapper Types
580
+
581
+ ZIO Blocks provides the `transform` method for creating schemas for wrapper types, such as newtypes, opaque types and value classes:
582
+
583
+ ```scala
584
+ final case class Schema[A](reflect: Reflect.Bound[A]) {
585
+ def transform[B](to: A => B, from: B => A): Schema[B] = ???
586
+ }
587
+ ```
588
+
589
+ The `transform` method allows you to define transformations that can fail by throwing `SchemaError` exceptions. Use it for both simple wrapper types and types with validation requirements.
590
+
591
+ Here are examples of both:
592
+
593
+ ```scala mdoc:compile-only
594
+ import zio.blocks.schema.{Schema, SchemaError}
595
+
596
+ // For types with validation (may fail)
597
+ case class Email(value: String)
598
+
599
+ object Email {
600
+ private val EmailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$".r
601
+
602
+ implicit val schema: Schema[Email] = Schema[String]
603
+ .transform(
604
+ {
605
+ case x @ EmailRegex(_*) => Email(x)
606
+ case _ => throw SchemaError.validationFailed("Invalid email format")
607
+ },
608
+ _.value
609
+ )
610
+ }
611
+
612
+ // For total transformations (never fail)
613
+ case class UserId(value: Long)
614
+
615
+ object UserId {
616
+ implicit val schema: Schema[UserId] = Schema[Long]
617
+ .transform(UserId(_), _.value)
618
+ }
619
+ ```