@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,409 @@
1
+ ---
2
+ id: syntax
3
+ title: "Extension Syntax"
4
+ ---
5
+
6
+ ZIO Blocks provides convenient extension methods on any value that has a `Schema`. These methods give you fluent, type-safe access to JSON encoding/decoding, pretty-printing, and patching operations directly on your values.
7
+
8
+ ## Import
9
+
10
+ To use the extension syntax, import the schema package:
11
+
12
+ ```scala mdoc:compile-only
13
+ import zio.blocks.schema._
14
+ ```
15
+
16
+ This brings the extension methods into scope for any type with an implicit `Schema` instance.
17
+
18
+ ## Quick Example
19
+
20
+ ```scala mdoc:compile-only
21
+ import zio.blocks.schema._
22
+
23
+ case class Person(name: String, age: Int)
24
+ object Person {
25
+ implicit val schema: Schema[Person] = Schema.derived
26
+ }
27
+
28
+ val alice = Person("Alice", 30)
29
+
30
+ // Convert to JSON
31
+ val json = alice.toJson // Json AST
32
+ val jsonStr = alice.toJsonString // {"name":"Alice","age":30}
33
+ val jsonBytes = alice.toJsonBytes // Array[Byte]
34
+
35
+ // Parse from JSON
36
+ val parsed = """{"name":"Bob","age":25}""".fromJson[Person]
37
+ // Right(Person("Bob", 25))
38
+
39
+ // Pretty-print
40
+ val shown = alice.show // Record { name = Alice, age = 30 }
41
+
42
+ // Compute and apply patches
43
+ val bob = Person("Bob", 30)
44
+ val patch = alice.diff(bob) // Patch that changes name
45
+ val result = alice.applyPatch(patch) // Person("Bob", 30)
46
+ ```
47
+
48
+ ## JSON Encoding Methods
49
+
50
+ ### toJson
51
+
52
+ Converts a value to a `Json` AST (abstract syntax tree):
53
+
54
+ ```scala mdoc:compile-only
55
+ import zio.blocks.schema._
56
+
57
+ case class Point(x: Int, y: Int)
58
+ object Point {
59
+ implicit val schema: Schema[Point] = Schema.derived
60
+ }
61
+
62
+ val point = Point(10, 20)
63
+ val json = point.toJson
64
+ // Json.Object(Vector("x" -> Json.Number(10), "y" -> Json.Number(20)))
65
+
66
+ // Navigate and extract values
67
+ json.get("x").as[Int] // Right(10)
68
+ json.get("y").as[Int] // Right(20)
69
+ ```
70
+
71
+ ### toJsonString
72
+
73
+ Converts a value directly to a JSON string:
74
+
75
+ ```scala mdoc:compile-only
76
+ import zio.blocks.schema._
77
+
78
+ case class User(name: String, email: String)
79
+ object User {
80
+ implicit val schema: Schema[User] = Schema.derived
81
+ }
82
+
83
+ val user = User("Alice", "alice@example.com")
84
+ val jsonStr = user.toJsonString
85
+ // {"name":"Alice","email":"alice@example.com"}
86
+ ```
87
+
88
+ ### toJsonBytes
89
+
90
+ Converts a value to a UTF-8 encoded byte array. This is useful for efficient serialization when working with binary protocols or network I/O:
91
+
92
+ ```scala mdoc:compile-only
93
+ import zio.blocks.schema._
94
+
95
+ case class Message(id: Long, content: String)
96
+ object Message {
97
+ implicit val schema: Schema[Message] = Schema.derived
98
+ }
99
+
100
+ val msg = Message(42, "Hello, world!")
101
+ val bytes: Array[Byte] = msg.toJsonBytes
102
+
103
+ // Useful for sending over the wire
104
+ // socket.write(bytes)
105
+ ```
106
+
107
+ ## JSON Decoding Methods
108
+
109
+ ### fromJson (on String)
110
+
111
+ Parses a JSON string into a typed value:
112
+
113
+ ```scala mdoc:compile-only
114
+ import zio.blocks.schema._
115
+
116
+ case class Config(host: String, port: Int)
117
+ object Config {
118
+ implicit val schema: Schema[Config] = Schema.derived
119
+ }
120
+
121
+ val jsonStr = """{"host":"localhost","port":8080}"""
122
+ val result: Either[SchemaError, Config] = jsonStr.fromJson[Config]
123
+ // Right(Config("localhost", 8080))
124
+
125
+ // Handle parsing errors
126
+ val invalid = """{"host":"localhost"}""" // missing port
127
+ val error = invalid.fromJson[Config]
128
+ // Left(SchemaError(...))
129
+ ```
130
+
131
+ ### fromJson (on Array[Byte])
132
+
133
+ Parses a UTF-8 byte array into a typed value:
134
+
135
+ ```scala mdoc:compile-only
136
+ import zio.blocks.schema._
137
+ import java.nio.charset.StandardCharsets
138
+
139
+ case class Event(name: String, timestamp: Long)
140
+ object Event {
141
+ implicit val schema: Schema[Event] = Schema.derived
142
+ }
143
+
144
+ val jsonBytes = """{"name":"click","timestamp":1234567890}"""
145
+ .getBytes(StandardCharsets.UTF_8)
146
+
147
+ val result: Either[SchemaError, Event] = jsonBytes.fromJson[Event]
148
+ // Right(Event("click", 1234567890))
149
+ ```
150
+
151
+ ## Pretty-Printing
152
+
153
+ ### show
154
+
155
+ Converts a value to a human-readable string representation using `DynamicValue`:
156
+
157
+ ```scala mdoc:compile-only
158
+ import zio.blocks.schema._
159
+
160
+ case class Address(street: String, city: String, zip: String)
161
+ object Address {
162
+ implicit val schema: Schema[Address] = Schema.derived
163
+ }
164
+
165
+ val addr = Address("123 Main St", "Springfield", "12345")
166
+ val shown = addr.show
167
+ // Record { street = 123 Main St, city = Springfield, zip = 12345 }
168
+ ```
169
+
170
+ This is useful for debugging and logging, as it provides a consistent, schema-aware representation of any value.
171
+
172
+ ## Patching Operations
173
+
174
+ ZIO Blocks includes a powerful patching system for computing and applying differences between values.
175
+
176
+ ### diff
177
+
178
+ Computes the difference between two values, returning a `Patch`:
179
+
180
+ ```scala mdoc:compile-only
181
+ import zio.blocks.schema._
182
+
183
+ case class Product(name: String, price: Double, stock: Int)
184
+ object Product {
185
+ implicit val schema: Schema[Product] = Schema.derived
186
+ }
187
+
188
+ val before = Product("Widget", 9.99, 100)
189
+ val after = Product("Widget", 12.99, 95)
190
+
191
+ val patch = before.diff(after)
192
+ // Patch contains: price changed from 9.99 to 12.99, stock from 100 to 95
193
+
194
+ patch.isEmpty // false
195
+ ```
196
+
197
+ An identical comparison produces an empty patch:
198
+
199
+ ```scala mdoc:compile-only
200
+ import zio.blocks.schema._
201
+
202
+ case class Item(id: Int, name: String)
203
+ object Item {
204
+ implicit val schema: Schema[Item] = Schema.derived
205
+ }
206
+
207
+ val item = Item(1, "Example")
208
+ val samePatch = item.diff(item)
209
+ samePatch.isEmpty // true
210
+ ```
211
+
212
+ ### applyPatch
213
+
214
+ Applies a patch to a value, returning the modified value. Uses lenient mode by default, which means operations that can't be applied are silently skipped:
215
+
216
+ ```scala mdoc:compile-only
217
+ import zio.blocks.schema._
218
+
219
+ case class Counter(name: String, value: Int)
220
+ object Counter {
221
+ implicit val schema: Schema[Counter] = Schema.derived
222
+ }
223
+
224
+ val counter = Counter("hits", 100)
225
+ val updated = Counter("hits", 150)
226
+
227
+ val patch = counter.diff(updated)
228
+ val result = counter.applyPatch(patch)
229
+ // Counter("hits", 150)
230
+ ```
231
+
232
+ ### applyPatchStrict
233
+
234
+ Applies a patch strictly, returning an `Either` that contains an error if any operation fails:
235
+
236
+ ```scala mdoc:compile-only
237
+ import zio.blocks.schema._
238
+ import zio.blocks.schema.patch.Patch
239
+
240
+ case class Record(id: String, version: Int)
241
+ object Record {
242
+ implicit val schema: Schema[Record] = Schema.derived
243
+ }
244
+
245
+ val record = Record("abc", 1)
246
+ val newRecord = Record("abc", 2)
247
+ val patch = record.diff(newRecord)
248
+
249
+ val result: Either[SchemaError, Record] = record.applyPatchStrict(patch)
250
+ // Right(Record("abc", 2))
251
+
252
+ // Empty patch also succeeds
253
+ val emptyResult = record.applyPatchStrict(Patch.empty[Record])
254
+ // Right(Record("abc", 1))
255
+ ```
256
+
257
+ ## Roundtrip Examples
258
+
259
+ ### JSON Roundtrip
260
+
261
+ ```scala mdoc:compile-only
262
+ import zio.blocks.schema._
263
+
264
+ case class Order(id: Long, items: List[String], total: BigDecimal)
265
+ object Order {
266
+ implicit val schema: Schema[Order] = Schema.derived
267
+ }
268
+
269
+ val order = Order(12345, List("apple", "banana"), BigDecimal("19.99"))
270
+
271
+ // String roundtrip
272
+ val jsonStr = order.toJsonString
273
+ val decoded1 = jsonStr.fromJson[Order]
274
+ // Right(Order(12345, List("apple", "banana"), 19.99))
275
+
276
+ // Bytes roundtrip
277
+ val jsonBytes = order.toJsonBytes
278
+ val decoded2 = jsonBytes.fromJson[Order]
279
+ // Right(Order(12345, List("apple", "banana"), 19.99))
280
+ ```
281
+
282
+ ### Patch Roundtrip
283
+
284
+ ```scala mdoc:compile-only
285
+ import zio.blocks.schema._
286
+
287
+ case class Settings(theme: String, fontSize: Int, notifications: Boolean)
288
+ object Settings {
289
+ implicit val schema: Schema[Settings] = Schema.derived
290
+ }
291
+
292
+ val defaults = Settings("light", 14, true)
293
+ val customized = Settings("dark", 16, false)
294
+
295
+ // Compute patch and apply
296
+ val patch = defaults.diff(customized)
297
+ val result = defaults.applyPatch(patch)
298
+ // Settings("dark", 16, false)
299
+
300
+ assert(result == customized)
301
+ ```
302
+
303
+ ## Edge Cases
304
+
305
+ ### Special Characters
306
+
307
+ The JSON encoding handles special characters, Unicode, and escape sequences correctly:
308
+
309
+ ```scala mdoc:compile-only
310
+ import zio.blocks.schema._
311
+
312
+ case class Text(content: String)
313
+ object Text {
314
+ implicit val schema: Schema[Text] = Schema.derived
315
+ }
316
+
317
+ // Special characters
318
+ val special = Text("""John "Jack" O'Brien""")
319
+ val json1 = special.toJsonString
320
+ val decoded1 = json1.fromJson[Text]
321
+ // Right(Text("John \"Jack\" O'Brien"))
322
+
323
+ // Unicode
324
+ val unicode = Text("日本語テキスト")
325
+ val json2 = unicode.toJsonString
326
+ val decoded2 = json2.fromJson[Text]
327
+ // Right(Text("日本語テキスト"))
328
+ ```
329
+
330
+ ### Empty and Null Values
331
+
332
+ ```scala mdoc:compile-only
333
+ import zio.blocks.schema._
334
+
335
+ case class Profile(name: String, bio: Option[String])
336
+ object Profile {
337
+ implicit val schema: Schema[Profile] = Schema.derived
338
+ }
339
+
340
+ // Empty strings
341
+ val empty = Profile("", None)
342
+ val json = empty.toJsonString
343
+ val decoded = json.fromJson[Profile]
344
+ // Right(Profile("", None))
345
+
346
+ // Optional fields
347
+ val withBio = Profile("Alice", Some("Developer"))
348
+ val withoutBio = Profile("Bob", None)
349
+ ```
350
+
351
+ ## Scala 2 vs Scala 3
352
+
353
+ The extension syntax works identically in both Scala 2 and Scala 3, but the implementation differs:
354
+
355
+ ### Scala 3 (Extension Methods)
356
+
357
+ ```scala
358
+ extension [A](self: A) {
359
+ def toJson(using schema: Schema[A]): Json = ...
360
+ def show(using schema: Schema[A]): String = ...
361
+ def diff(that: A)(using schema: Schema[A]): Patch[A] = ...
362
+ // ...
363
+ }
364
+
365
+ extension (self: String) {
366
+ def fromJson[A](using schema: Schema[A]): Either[SchemaError, A] = ...
367
+ }
368
+
369
+ extension (self: Array[Byte]) {
370
+ def fromJson[A](using schema: Schema[A]): Either[SchemaError, A] = ...
371
+ }
372
+ ```
373
+
374
+ ### Scala 2 (Implicit Classes)
375
+
376
+ ```scala
377
+ implicit final class SchemaValueOps[A](private val self: A) {
378
+ def toJson(implicit schema: Schema[A]): Json = ...
379
+ def show(implicit schema: Schema[A]): String = ...
380
+ def diff(that: A)(implicit schema: Schema[A]): Patch[A] = ...
381
+ // ...
382
+ }
383
+
384
+ implicit final class StringSchemaOps(private val self: String) {
385
+ def fromJson[A](implicit schema: Schema[A]): Either[SchemaError, A] = ...
386
+ }
387
+
388
+ implicit final class ByteArraySchemaOps(private val self: Array[Byte]) {
389
+ def fromJson[A](implicit schema: Schema[A]): Either[SchemaError, A] = ...
390
+ }
391
+ ```
392
+
393
+ The API is the same—just import `zio.blocks.schema._` and the appropriate syntax is provided for your Scala version.
394
+
395
+ ## Method Reference
396
+
397
+ | Method | Receiver | Return Type | Description |
398
+ |--------|----------|-------------|-------------|
399
+ | `toJson` | `A` | `Json` | Convert to JSON AST |
400
+ | `toJsonString` | `A` | `String` | Convert to JSON string |
401
+ | `toJsonBytes` | `A` | `Array[Byte]` | Convert to UTF-8 bytes |
402
+ | `fromJson[A]` | `String` | `Either[SchemaError, A]` | Parse JSON string |
403
+ | `fromJson[A]` | `Array[Byte]` | `Either[SchemaError, A]` | Parse JSON bytes |
404
+ | `show` | `A` | `String` | Pretty-print via DynamicValue |
405
+ | `diff` | `A` | `Patch[A]` | Compute patch to another value |
406
+ | `applyPatch` | `A` | `A` | Apply patch (lenient) |
407
+ | `applyPatchStrict` | `A` | `Either[SchemaError, A]` | Apply patch (strict) |
408
+
409
+ All methods require an implicit/given `Schema[A]` in scope.