@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.
- package/index.md +426 -0
- package/package.json +6 -0
- package/path-interpolator.md +645 -0
- package/reference/binding.md +364 -0
- package/reference/chunk.md +576 -0
- package/reference/context.md +157 -0
- package/reference/docs.md +524 -0
- package/reference/dynamic-value.md +823 -0
- package/reference/formats.md +640 -0
- package/reference/json-schema.md +626 -0
- package/reference/json.md +979 -0
- package/reference/modifier.md +276 -0
- package/reference/optics.md +1613 -0
- package/reference/patch.md +631 -0
- package/reference/reflect-transform.md +387 -0
- package/reference/reflect.md +521 -0
- package/reference/registers.md +282 -0
- package/reference/schema-evolution.md +540 -0
- package/reference/schema.md +619 -0
- package/reference/syntax.md +409 -0
- package/reference/type-class-derivation-internals.md +632 -0
- package/reference/typeid.md +900 -0
- package/reference/validation.md +458 -0
- package/scope.md +627 -0
- package/sidebars.js +30 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: schema-evolution
|
|
3
|
+
title: "Schema Evolution"
|
|
4
|
+
sidebar_label: "Schema Evolution"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Schema evolution is a common challenge in distributed systems where data structures change over time. ZIO Blocks provides two type classes—`Into` and `As`—that enable type-safe, compile-time verified transformations between different versions of your data types.
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
When your application evolves, you often need to:
|
|
12
|
+
|
|
13
|
+
- Add new fields to existing types
|
|
14
|
+
- Remove deprecated fields
|
|
15
|
+
- Rename fields
|
|
16
|
+
- Change field types (e.g., `Int` → `Long`)
|
|
17
|
+
- Convert between different representations of the same concept
|
|
18
|
+
|
|
19
|
+
ZIO Blocks handles these transformations with:
|
|
20
|
+
|
|
21
|
+
| Type Class | Direction | Use Case |
|
|
22
|
+
|------------|-----------|----------|
|
|
23
|
+
| `Into[A, B]` | One-way (A → B) | Migrations, API responses, data import |
|
|
24
|
+
| `As[A, B]` | Bidirectional (A ↔ B) | Round-trip serialization, data sync |
|
|
25
|
+
|
|
26
|
+
## Into[A, B] - One-Way Conversion
|
|
27
|
+
|
|
28
|
+
`Into[A, B]` represents a one-way conversion from type `A` to type `B` with validation:
|
|
29
|
+
|
|
30
|
+
```scala
|
|
31
|
+
trait Into[-A, +B] {
|
|
32
|
+
def into(a: A): Either[SchemaError, B]
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Basic Usage
|
|
37
|
+
|
|
38
|
+
```scala
|
|
39
|
+
import zio.blocks.schema.Into
|
|
40
|
+
|
|
41
|
+
// Version 1 of our API
|
|
42
|
+
case class PersonV1(name: String, age: Int)
|
|
43
|
+
|
|
44
|
+
// Version 2 adds email and changes age to Long
|
|
45
|
+
case class PersonV2(name: String, age: Long, email: Option[String])
|
|
46
|
+
|
|
47
|
+
// Derive the conversion automatically
|
|
48
|
+
val migrate: Into[PersonV1, PersonV2] = Into.derived[PersonV1, PersonV2]
|
|
49
|
+
|
|
50
|
+
// Use it
|
|
51
|
+
val v1 = PersonV1("Alice", 30)
|
|
52
|
+
val v2 = migrate.into(v1)
|
|
53
|
+
// Right(PersonV2("Alice", 30L, None))
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Field Matching Rules
|
|
57
|
+
|
|
58
|
+
The macro matches fields using the following priority:
|
|
59
|
+
|
|
60
|
+
1. **Exact match**: Same name + same type
|
|
61
|
+
2. **Name match with coercion**: Same name + convertible type (e.g., `Int` → `Long`)
|
|
62
|
+
3. **Unique type match**: Type appears only once in both source and target
|
|
63
|
+
4. **Position + type match**: Positional correspondence with matching type
|
|
64
|
+
|
|
65
|
+
### Handling Missing Fields
|
|
66
|
+
|
|
67
|
+
When the target has fields not present in the source:
|
|
68
|
+
|
|
69
|
+
```scala
|
|
70
|
+
case class Source(name: String)
|
|
71
|
+
case class Target(name: String, age: Int = 25, nickname: Option[String])
|
|
72
|
+
|
|
73
|
+
val convert = Into.derived[Source, Target]
|
|
74
|
+
convert.into(Source("Bob"))
|
|
75
|
+
// Right(Target("Bob", 25, None))
|
|
76
|
+
// ↑ ↑
|
|
77
|
+
// default Option defaults to None
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
- **Default values**: Used when target field has a default
|
|
81
|
+
- **Option types**: Default to `None` when not present in source
|
|
82
|
+
|
|
83
|
+
### Numeric Conversions
|
|
84
|
+
|
|
85
|
+
Built-in support for numeric widening and narrowing:
|
|
86
|
+
|
|
87
|
+
```scala
|
|
88
|
+
// Widening (lossless) - always succeeds
|
|
89
|
+
Into[Byte, Short] // Byte → Short
|
|
90
|
+
Into[Int, Long] // Int → Long
|
|
91
|
+
Into[Float, Double] // Float → Double
|
|
92
|
+
|
|
93
|
+
// Narrowing (with validation) - may fail at runtime
|
|
94
|
+
Into[Long, Int] // Fails if value > Int.MaxValue or < Int.MinValue
|
|
95
|
+
Into[Double, Float] // Fails if value out of Float range
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Example with validation failure:
|
|
99
|
+
|
|
100
|
+
```scala
|
|
101
|
+
case class BigNumbers(value: Long)
|
|
102
|
+
case class SmallNumbers(value: Int)
|
|
103
|
+
|
|
104
|
+
val narrow = Into.derived[BigNumbers, SmallNumbers]
|
|
105
|
+
|
|
106
|
+
narrow.into(BigNumbers(42L))
|
|
107
|
+
// Right(SmallNumbers(42))
|
|
108
|
+
|
|
109
|
+
narrow.into(BigNumbers(Long.MaxValue))
|
|
110
|
+
// Left(SchemaError: "Value 9223372036854775807 is out of range for Int")
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Collection Conversions
|
|
114
|
+
|
|
115
|
+
Automatic conversion between collection types:
|
|
116
|
+
|
|
117
|
+
```scala
|
|
118
|
+
case class ListData(items: List[Int])
|
|
119
|
+
case class VectorData(items: Vector[Long])
|
|
120
|
+
|
|
121
|
+
Into.derived[ListData, VectorData]
|
|
122
|
+
// Converts List → Vector AND Int → Long
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Supported conversions:
|
|
126
|
+
- `List`, `Vector`, `Set`, `Seq` (interchangeable)
|
|
127
|
+
- `Array` ↔ `Iterable`
|
|
128
|
+
- `Map[K1, V1]` → `Map[K2, V2]`
|
|
129
|
+
- `Option[A]` → `Option[B]`
|
|
130
|
+
- `Either[L1, R1]` → `Either[L2, R2]`
|
|
131
|
+
|
|
132
|
+
:::note
|
|
133
|
+
Converting to `Set` may remove duplicates. Converting from `Set` does not preserve any particular ordering.
|
|
134
|
+
:::
|
|
135
|
+
|
|
136
|
+
### Sealed Trait / Enum Conversions
|
|
137
|
+
|
|
138
|
+
Convert between coproduct types (sealed traits, enums):
|
|
139
|
+
|
|
140
|
+
```scala
|
|
141
|
+
// Scala 2
|
|
142
|
+
sealed trait StatusV1
|
|
143
|
+
object StatusV1 {
|
|
144
|
+
case object Active extends StatusV1
|
|
145
|
+
case object Inactive extends StatusV1
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
sealed trait StatusV2
|
|
149
|
+
object StatusV2 {
|
|
150
|
+
case object Active extends StatusV2
|
|
151
|
+
case object Inactive extends StatusV2
|
|
152
|
+
case object Pending extends StatusV2 // New case added
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Scala 3
|
|
156
|
+
enum StatusV1 { case Active, Inactive }
|
|
157
|
+
enum StatusV2 { case Active, Inactive, Pending }
|
|
158
|
+
|
|
159
|
+
// Works - all V1 cases exist in V2
|
|
160
|
+
Into.derived[StatusV1, StatusV2]
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Cases are matched by:
|
|
164
|
+
1. **Name**: Case names must match
|
|
165
|
+
2. **Signature**: For case classes, field types must be convertible
|
|
166
|
+
|
|
167
|
+
### Nested Type Conversions
|
|
168
|
+
|
|
169
|
+
For nested types, provide implicit `Into` instances:
|
|
170
|
+
|
|
171
|
+
```scala
|
|
172
|
+
case class AddressV1(street: String, zip: Int)
|
|
173
|
+
case class AddressV2(street: String, zip: Long)
|
|
174
|
+
|
|
175
|
+
case class PersonV1(name: String, address: AddressV1)
|
|
176
|
+
case class PersonV2(name: String, address: AddressV2)
|
|
177
|
+
|
|
178
|
+
// The macro automatically uses Into[AddressV1, AddressV2] for the nested field
|
|
179
|
+
val convert = Into.derived[PersonV1, PersonV2]
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Error Accumulation
|
|
183
|
+
|
|
184
|
+
When multiple fields fail validation, all errors are accumulated:
|
|
185
|
+
|
|
186
|
+
```scala
|
|
187
|
+
case class Source(a: Long, b: Long, c: Long)
|
|
188
|
+
case class Target(a: Int, b: Int, c: Int)
|
|
189
|
+
|
|
190
|
+
val convert = Into.derived[Source, Target]
|
|
191
|
+
convert.into(Source(Long.MaxValue, Long.MinValue, 42L))
|
|
192
|
+
// Left(SchemaError containing errors for BOTH 'a' and 'b')
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## As[A, B] - Bidirectional Conversion
|
|
196
|
+
|
|
197
|
+
`As[A, B]` extends `Into[A, B]` with a reverse conversion:
|
|
198
|
+
|
|
199
|
+
```scala
|
|
200
|
+
trait As[A, B] extends Into[A, B] {
|
|
201
|
+
def from(input: B): Either[SchemaError, A]
|
|
202
|
+
def reverse: As[B, A]
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Basic Usage
|
|
207
|
+
|
|
208
|
+
```scala
|
|
209
|
+
import zio.blocks.schema.As
|
|
210
|
+
|
|
211
|
+
case class Point2D(x: Int, y: Int)
|
|
212
|
+
case class Coordinate(x: Int, y: Int)
|
|
213
|
+
|
|
214
|
+
val convert: As[Point2D, Coordinate] = As.derived[Point2D, Coordinate]
|
|
215
|
+
|
|
216
|
+
// Both directions work
|
|
217
|
+
convert.into(Point2D(1, 2)) // Right(Coordinate(1, 2))
|
|
218
|
+
convert.from(Coordinate(3, 4)) // Right(Point2D(3, 4))
|
|
219
|
+
|
|
220
|
+
// Swap directions
|
|
221
|
+
val reversed: As[Coordinate, Point2D] = convert.reverse
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Restrictions for As
|
|
225
|
+
|
|
226
|
+
`As` has stricter requirements than `Into` to guarantee round-trip safety:
|
|
227
|
+
|
|
228
|
+
#### ❌ No Default Values on Non-Matching Fields
|
|
229
|
+
|
|
230
|
+
Default values break round-trip when a field with a default exists in one type but not in the other:
|
|
231
|
+
|
|
232
|
+
```scala
|
|
233
|
+
case class WithDefault(name: String, age: Int = 25)
|
|
234
|
+
case class NoDefault(name: String)
|
|
235
|
+
|
|
236
|
+
// This will NOT compile (age has default but doesn't exist in NoDefault):
|
|
237
|
+
As.derived[WithDefault, NoDefault]
|
|
238
|
+
// Error: "Cannot derive As[...]: Default values break round-trip guarantee"
|
|
239
|
+
|
|
240
|
+
// Use Into instead for one-way conversion:
|
|
241
|
+
Into.derived[NoDefault, WithDefault] // ✓ Works
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
However, default values ARE allowed when the field exists in both types:
|
|
245
|
+
|
|
246
|
+
```scala
|
|
247
|
+
case class PersonA(name: String, age: Int = 25)
|
|
248
|
+
case class PersonB(name: String, age: Int)
|
|
249
|
+
|
|
250
|
+
// This WILL compile (age exists in both types):
|
|
251
|
+
As.derived[PersonA, PersonB] // ✓ Works
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
#### ✅ Option Fields Are Allowed
|
|
255
|
+
|
|
256
|
+
Option fields work because `None` round-trips correctly:
|
|
257
|
+
|
|
258
|
+
```scala
|
|
259
|
+
case class TypeA(name: String, nickname: Option[String])
|
|
260
|
+
case class TypeB(name: String)
|
|
261
|
+
|
|
262
|
+
As.derived[TypeA, TypeB] // ✓ Works
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
#### ✅ Numeric Coercions Must Be Invertible
|
|
266
|
+
|
|
267
|
+
Numeric types can be coerced if the conversion works in both directions:
|
|
268
|
+
|
|
269
|
+
```scala
|
|
270
|
+
case class IntVersion(value: Int)
|
|
271
|
+
case class LongVersion(value: Long)
|
|
272
|
+
|
|
273
|
+
As.derived[IntVersion, LongVersion]
|
|
274
|
+
// ✓ Works: Int → Long (widening) and Long → Int (narrowing with validation)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Using As Where Into Is Expected
|
|
278
|
+
|
|
279
|
+
Since `As[A, B]` extends `Into[A, B]`, you can use it anywhere an `Into` is required:
|
|
280
|
+
|
|
281
|
+
```scala
|
|
282
|
+
def migrate[A, B](data: A)(implicit into: Into[A, B]): Either[SchemaError, B] =
|
|
283
|
+
into.into(data)
|
|
284
|
+
|
|
285
|
+
implicit val as: As[Point2D, Coordinate] = As.derived
|
|
286
|
+
|
|
287
|
+
migrate(Point2D(1, 2)) // Uses As as an Into
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## ZIO Prelude Newtype Support
|
|
291
|
+
|
|
292
|
+
Both `Into` and `As` automatically detect and validate ZIO Prelude newtypes:
|
|
293
|
+
|
|
294
|
+
```scala
|
|
295
|
+
import zio.prelude._
|
|
296
|
+
|
|
297
|
+
object Age extends Subtype[Int] {
|
|
298
|
+
override def assertion = assert(between(0, 150))
|
|
299
|
+
}
|
|
300
|
+
type Age = Age.Type
|
|
301
|
+
|
|
302
|
+
case class PersonRaw(name: String, age: Int)
|
|
303
|
+
case class PersonValidated(name: String, age: Age)
|
|
304
|
+
|
|
305
|
+
val validate = Into.derived[PersonRaw, PersonValidated]
|
|
306
|
+
|
|
307
|
+
validate.into(PersonRaw("Alice", 30))
|
|
308
|
+
// Right(PersonValidated("Alice", Age(30)))
|
|
309
|
+
|
|
310
|
+
validate.into(PersonRaw("Bob", -5))
|
|
311
|
+
// Left(SchemaError: "Validation failed for field 'age': ...")
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
The macro automatically:
|
|
315
|
+
1. Detects that `Age` is a ZIO Prelude newtype
|
|
316
|
+
2. Calls `Age.make(value)` for validation
|
|
317
|
+
3. Converts `Validation` result to `Either[SchemaError, _]`
|
|
318
|
+
|
|
319
|
+
## Scala 3 Opaque Type Support
|
|
320
|
+
|
|
321
|
+
In Scala 3, opaque types with companion validation are supported:
|
|
322
|
+
|
|
323
|
+
```scala
|
|
324
|
+
opaque type Email = String
|
|
325
|
+
object Email {
|
|
326
|
+
def apply(value: String): Either[String, Email] =
|
|
327
|
+
if (value.contains("@")) Right(value)
|
|
328
|
+
else Left(s"Invalid email: $value")
|
|
329
|
+
|
|
330
|
+
def unsafe(value: String): Email = value
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
case class UserRaw(name: String, email: String)
|
|
334
|
+
case class UserValidated(name: String, email: Email)
|
|
335
|
+
|
|
336
|
+
Into.derived[UserRaw, UserValidated]
|
|
337
|
+
// Automatically uses Email.apply for validation
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
The macro looks for:
|
|
341
|
+
1. `apply(value: Underlying): Either[_, OpaqueType]` - validation method
|
|
342
|
+
2. `unsafe(value: Underlying): OpaqueType` - fallback without validation
|
|
343
|
+
|
|
344
|
+
## Structural Type Support
|
|
345
|
+
|
|
346
|
+
ZIO Blocks supports conversions involving structural types on JVM only, as they require reflection.
|
|
347
|
+
|
|
348
|
+
### Platform Compatibility Matrix
|
|
349
|
+
|
|
350
|
+
| Conversion | JVM | JS | Notes |
|
|
351
|
+
|------------|-----|-----|-------|
|
|
352
|
+
| Product → Structural | ✅ | ❌ | JVM only (reflection) |
|
|
353
|
+
| Structural → Product | ✅ | ❌ | JVM only (reflection) |
|
|
354
|
+
|
|
355
|
+
**Key insight**: Structural types require runtime reflection to access their members, which is only available on JVM. On JS, structural type conversions will fail at compile time with a helpful error message.
|
|
356
|
+
|
|
357
|
+
### Structural Types (JVM Only)
|
|
358
|
+
|
|
359
|
+
Structural types are types defined by their members rather than their name:
|
|
360
|
+
|
|
361
|
+
```scala
|
|
362
|
+
// JVM ONLY - will fail at compile time on JS
|
|
363
|
+
case class Person(name: String, age: Int)
|
|
364
|
+
|
|
365
|
+
// Structural type to case class
|
|
366
|
+
val into = Into.derived[{ def name: String; def age: Int }, Person]
|
|
367
|
+
|
|
368
|
+
// Case class to structural type
|
|
369
|
+
val toStructural = Into.derived[Person, { def name: String; def age: Int }]
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**Compile-time error on non-JVM platforms:**
|
|
373
|
+
```
|
|
374
|
+
Cannot derive Into[..., Person]: Structural type conversions are not supported on JS.
|
|
375
|
+
|
|
376
|
+
Structural types require reflection APIs (getClass.getMethod) which are only available on JVM.
|
|
377
|
+
|
|
378
|
+
Consider:
|
|
379
|
+
- Using a case class instead of a structural type
|
|
380
|
+
- Using a tuple instead of a structural type
|
|
381
|
+
- Only using structural type conversions in JVM-only code
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Working with Structural Types
|
|
385
|
+
|
|
386
|
+
```scala
|
|
387
|
+
import scala.language.reflectiveCalls
|
|
388
|
+
|
|
389
|
+
// Create a structural type instance
|
|
390
|
+
def makePerson(n: String, a: Int): { def name: String; def age: Int } = new {
|
|
391
|
+
def name: String = n
|
|
392
|
+
def age: Int = a
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
case class Person(name: String, age: Int)
|
|
396
|
+
|
|
397
|
+
// Convert structural → case class
|
|
398
|
+
val into = Into.derived[{ def name: String; def age: Int }, Person]
|
|
399
|
+
val result = into.into(makePerson("Alice", 30))
|
|
400
|
+
// Right(Person("Alice", 30))
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
## Scala 2 vs Scala 3 Differences
|
|
404
|
+
|
|
405
|
+
| Feature | Scala 2 | Scala 3 |
|
|
406
|
+
|---------|---------|---------|
|
|
407
|
+
| Derivation syntax | `Into.derived[A, B]` | `Into.derived[A, B]` |
|
|
408
|
+
| Enum support | Sealed traits only | Scala 3 enums + sealed traits |
|
|
409
|
+
| Opaque types | N/A | ✅ Supported |
|
|
410
|
+
| Structural types | JVM only (reflection) | JVM only (reflection) |
|
|
411
|
+
| ZIO Prelude newtypes | ✅ `assert { ... }` syntax | ✅ `override def assertion` syntax |
|
|
412
|
+
| Error messages | Detailed macro errors | Detailed macro errors |
|
|
413
|
+
|
|
414
|
+
### ZIO Prelude Newtype Syntax
|
|
415
|
+
|
|
416
|
+
**Scala 2:**
|
|
417
|
+
```scala
|
|
418
|
+
object Age extends Subtype[Int] {
|
|
419
|
+
override def assertion = assert {
|
|
420
|
+
between(0, 150)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
**Scala 3:**
|
|
426
|
+
```scala
|
|
427
|
+
object Age extends Subtype[Int] {
|
|
428
|
+
override def assertion: Assertion[Int] =
|
|
429
|
+
zio.prelude.Assertion.between(0, 150)
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
## Best Practices
|
|
434
|
+
|
|
435
|
+
### 1. Prefer As When Round-Trip Is Required
|
|
436
|
+
|
|
437
|
+
```scala
|
|
438
|
+
// For data sync, use As
|
|
439
|
+
val sync: As[LocalModel, RemoteModel] = As.derived
|
|
440
|
+
|
|
441
|
+
// For one-way migrations, use Into
|
|
442
|
+
val migrate: Into[OldFormat, NewFormat] = Into.derived
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### 2. Use Option for Truly Optional Fields
|
|
446
|
+
|
|
447
|
+
```scala
|
|
448
|
+
// Good: Optional field with Option
|
|
449
|
+
case class V2(name: String, email: Option[String])
|
|
450
|
+
|
|
451
|
+
// Avoid: Default values break As derivation
|
|
452
|
+
case class V2(name: String, email: String = "")
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### 3. Provide Explicit Instances for Complex Nested Types
|
|
456
|
+
|
|
457
|
+
```scala
|
|
458
|
+
// When nested types need custom logic
|
|
459
|
+
implicit val addressConvert: Into[AddressV1, AddressV2] =
|
|
460
|
+
Into.derived[AddressV1, AddressV2]
|
|
461
|
+
|
|
462
|
+
// Now this works automatically
|
|
463
|
+
val personConvert = Into.derived[PersonV1, PersonV2]
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### 4. Structural Types Are JVM-Only
|
|
467
|
+
|
|
468
|
+
```scala
|
|
469
|
+
// JVM-only: Structural types require reflection
|
|
470
|
+
type PersonLike = { def name: String }
|
|
471
|
+
|
|
472
|
+
// For cross-platform code, use case classes instead
|
|
473
|
+
case class PersonLike(name: String)
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
## Complete Example
|
|
477
|
+
|
|
478
|
+
Here's a complete schema evolution example:
|
|
479
|
+
|
|
480
|
+
```scala
|
|
481
|
+
import zio.blocks.schema._
|
|
482
|
+
|
|
483
|
+
// API v1
|
|
484
|
+
object V1 {
|
|
485
|
+
case class Address(street: String, city: String)
|
|
486
|
+
case class Person(name: String, age: Int, address: Address)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// API v2 - adds fields, changes types
|
|
490
|
+
object V2 {
|
|
491
|
+
case class Address(street: String, city: String, country: String = "US")
|
|
492
|
+
case class Person(
|
|
493
|
+
name: String,
|
|
494
|
+
age: Long, // Changed from Int
|
|
495
|
+
address: Address,
|
|
496
|
+
email: Option[String] // New field
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Define conversions
|
|
501
|
+
object Migrations {
|
|
502
|
+
// Address: one-way (v2 has default for country)
|
|
503
|
+
implicit val addressMigrate: Into[V1.Address, V2.Address] =
|
|
504
|
+
Into.derived[V1.Address, V2.Address]
|
|
505
|
+
|
|
506
|
+
// Person: one-way (v2 has new optional field)
|
|
507
|
+
implicit val personMigrate: Into[V1.Person, V2.Person] =
|
|
508
|
+
Into.derived[V1.Person, V2.Person]
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Usage
|
|
512
|
+
import Migrations._
|
|
513
|
+
|
|
514
|
+
val oldPerson = V1.Person("Alice", 30, V1.Address("123 Main St", "NYC"))
|
|
515
|
+
val newPerson = personMigrate.into(oldPerson)
|
|
516
|
+
// Right(V2.Person("Alice", 30L, V2.Address("123 Main St", "NYC", "US"), None))
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
## Error Handling
|
|
520
|
+
|
|
521
|
+
All conversions return `Either[SchemaError, B]` for explicit error handling:
|
|
522
|
+
|
|
523
|
+
```scala
|
|
524
|
+
val result = migrate.into(oldData)
|
|
525
|
+
|
|
526
|
+
result match {
|
|
527
|
+
case Right(newData) =>
|
|
528
|
+
// Success - use newData
|
|
529
|
+
|
|
530
|
+
case Left(error) =>
|
|
531
|
+
// Handle validation/conversion failure
|
|
532
|
+
println(s"Migration failed: ${error.message}")
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
`SchemaError` provides:
|
|
537
|
+
- Detailed error messages
|
|
538
|
+
- Field path information
|
|
539
|
+
- Error accumulation (multiple errors combined)
|
|
540
|
+
|