@zio.dev/zio-blocks 0.0.21 → 0.0.22

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,392 @@
1
+ ---
2
+ id: dynamic-optic
3
+ title: "DynamicOptic"
4
+ ---
5
+
6
+ `DynamicOptic` is a runtime path through nested data structures in ZIO Blocks. It is the untyped,
7
+ dynamically-constructed counterpart to the typed [`Optic[S, A]`](./optics.md). Where typed optics are bound to specific
8
+ Scala types at compile time, a `DynamicOptic` is a sequence of **navigation steps** that can be built, composed, and
9
+ applied at runtime:
10
+
11
+ ```scala
12
+ import zio.blocks.schema._
13
+
14
+ // Build a path: .users[0].name
15
+ val path = DynamicOptic.root.field("users").at(0).field("name")
16
+
17
+ // Navigate a DynamicValue
18
+ val data = DynamicValue.Record(
19
+ "users" -> DynamicValue.Sequence(
20
+ DynamicValue.Record("name" -> DynamicValue.string("Alice"))
21
+ )
22
+ )
23
+
24
+ val result = data.get(path).one
25
+ // Right(DynamicValue.Primitive(PrimitiveValue.String("Alice")))
26
+ ```
27
+
28
+ ## Motivation
29
+
30
+ Most of the time you work with typed `Optic[S, A]` values — they are statically verified and provide type-safe
31
+ get/set/modify operations. `DynamicOptic` is useful when:
32
+
33
+ - **The path is determined at runtime** — user input, configuration, query parameters
34
+ - **You are working with `DynamicValue`** — navigating, modifying, or querying schema-less data
35
+ - **You need schema introspection** — walking a `Schema` or `Reflect` structure programmatically
36
+ - **Serializing optic paths** — `DynamicOptic` has its own `Schema[DynamicOptic]` and can be persisted
37
+
38
+ The relationship between typed and dynamic optics:
39
+
40
+ ```
41
+ Typed World Dynamic World
42
+ ┌──────────────────┐ ┌──────────────────┐
43
+ │ Optic[S, A] │─────────────│ DynamicOptic │
44
+ │ │ toDynamic │ │
45
+ │ Lens, Prism, │ │ Sequence of │
46
+ │ Optional, │ │ Node steps │
47
+ │ Traversal │ │ │
48
+ ├──────────────────┤ ├──────────────────┤
49
+ │ Operates on │ │ Operates on │
50
+ │ typed values │ │ DynamicValue, │
51
+ │ (case classes, │ │ Schema, │
52
+ │ sealed traits) │ │ Reflect │
53
+ └──────────────────┘ └──────────────────┘
54
+ ```
55
+
56
+ The `Optic[S, A]` and `DynamicOptic` types serve complementary roles in ZIO Blocks' optics system. `Optic[S, A]` provides compile-time type safety through macros or manual construction, operates on typed Scala values, and can be converted to a `DynamicOptic` via the `optic.toDynamic` method. In contrast, `DynamicOptic` performs runtime type checking and is constructed through a builder API or [path interpolator](../path-interpolator.md), operating directly on [DynamicValue](./dynamic-value.md), [Schema](./schema.md), and [Reflect](./reflect.md) representations.
57
+
58
+ ## Design & Structure
59
+
60
+ `DynamicOptic` is modeled as a case class wrapping an `IndexedSeq[Node]`, where each `Node` represents one step in a navigation path:
61
+
62
+ ```
63
+ DynamicOptic(nodes: IndexedSeq[Node])
64
+
65
+ sealed trait DynamicOptic.Node
66
+ ├── Field(name: String) — named field in a record
67
+ ├── Case(name: String) — specific case in a variant
68
+ ├── AtIndex(index: Int) — element at index in a sequence
69
+ ├── AtIndices(index: Seq[Int]) — elements at multiple indices
70
+ ├── AtMapKey(key: DynamicValue) — value at a specific map key
71
+ ├── AtMapKeys(keys: Seq[DynamicValue]) — values at multiple map keys
72
+ ├── Elements — all elements in a sequence (wildcard)
73
+ ├── MapKeys — all keys in a map (wildcard)
74
+ ├── MapValues — all values in a map (wildcard)
75
+ └── Wrapped — inner value of a wrapper/newtype
76
+ ```
77
+
78
+ Each of the `DynamicOptic` nodes represents a different way to navigate through a data structure. Here's a reference table for the nodes, their navigation semantics, and string representations:
79
+
80
+ | Node | Navigates | `toString` | `toScalaString` |
81
+ |-----------------------|-----------------------|--------------|---------------------|
82
+ | `Field("name")` | Record field | `.name` | `.name` |
83
+ | `Case("Email")` | Variant case | `<Email>` | `.when[Email]` |
84
+ | `AtIndex(0)` | Sequence element | `[0]` | `.at(0)` |
85
+ | `AtIndices(Seq(0,2))` | Multiple elements | `[0,2]` | `.atIndices(0, 2)` |
86
+ | `AtMapKey(k)` | Map entry by key | `{"host"}` | `.atKey("host")` |
87
+ | `AtMapKeys(ks)` | Multiple entries | `{"a", "b"}` | `.atKeys("a", "b")` |
88
+ | `Elements` | All sequence elements | `[*]` | `.each` |
89
+ | `MapKeys` | All map keys | `{*:}` | `.eachKey` |
90
+ | `MapValues` | All map values | `{*}` | `.eachValue` |
91
+ | `Wrapped` | Wrapper inner value | `.~` | `.wrapped` |
92
+
93
+ Key design decisions:
94
+
95
+ - **Map keys as `DynamicValue`** — Map keys can be any type (`String`, `Int`, `Boolean`, etc.), so `AtMapKey` stores the key as a `DynamicValue` to remain type-agnostic.
96
+ - **Dual rendering** — `toString` produces a compact interpolator syntax (`.field[0]{key}`), while `toScalaString` produces Scala method-call syntax (`.field.at(0).atKey(key)`). The compact format is designed to be copy-pasteable into the `p"..."` string interpolator, while the Scala format is used in error messages.
97
+ - **Every typed `Optic` has a `toDynamic` method** — This bridges the typed and untyped worlds, allowing any `Lens`, `Prism`, `Optional`, or `Traversal` to produce its `DynamicOptic` representation.
98
+
99
+ ## Construction
100
+
101
+ ### Starting Points
102
+
103
+ Every `DynamicOptic` starts from `root` (the identity/empty path) or one of the pre-built singletons:
104
+
105
+ ```scala
106
+ import zio.blocks.schema.DynamicOptic
107
+
108
+ val root = DynamicOptic.root // empty path: "."
109
+ val elems = DynamicOptic.elements // "[*]"
110
+ val keys = DynamicOptic.mapKeys // "{*:}"
111
+ val values = DynamicOptic.mapValues // "{*}"
112
+ val inner = DynamicOptic.wrapped // ".~"
113
+ ```
114
+
115
+ ### Builder Methods
116
+
117
+ Chain builder methods on `DynamicOptic.root` (or any existing optic) to construct paths fluently:
118
+
119
+ ```scala
120
+ import zio.blocks.schema._
121
+
122
+ // Navigate into a record field, then a sequence index, then another field
123
+ val path = DynamicOptic.root.field("users").at(0).field("name")
124
+ // toString: .users[0].name
125
+
126
+ // Navigate into a map with a typed key
127
+ val configPath = DynamicOptic.root.field("config").atKey("host")
128
+ // toString: .config{"host"}
129
+
130
+ // Navigate into a variant case, then a field
131
+ val resultPath = DynamicOptic.root.field("result").caseOf("Success").field("value")
132
+ // toString: .result<Success>.value
133
+
134
+ // Select all elements, then a field on each
135
+ val allEmails = DynamicOptic.root.field("users").elements.field("email")
136
+ // toString: .users[*].email
137
+ ```
138
+
139
+ | Method | Node Produced | Example |
140
+ |------------------|----------------------|-----------------------|
141
+ | `.field(name)` | `Field(name)` | `.field("street")` |
142
+ | `.caseOf(name)` | `Case(name)` | `.caseOf("Email")` |
143
+ | `.at(index)` | `AtIndex(index)` | `.at(0)` |
144
+ | `.atIndices(i*)` | `AtIndices(indices)` | `.atIndices(0, 2, 5)` |
145
+ | `.atKey[K](key)` | `AtMapKey(dv)` | `.atKey("host")` |
146
+ | `.atKeys[K](k*)` | `AtMapKeys(dvs)` | `.atKeys("a", "b")` |
147
+ | `.elements` | `Elements` | `.elements` |
148
+ | `.mapKeys` | `MapKeys` | `.mapKeys` |
149
+ | `.mapValues` | `MapValues` | `.mapValues` |
150
+ | `.wrapped` | `Wrapped` | `.wrapped` |
151
+
152
+ Note: `.atKey` and `.atKeys` require an implicit `Schema[K]` to convert the typed key to a `DynamicValue`.
153
+
154
+ ### Path Interpolator
155
+
156
+ The [`p"..."` path interpolator](../path-interpolator.md) provides a concise compile-time syntax for building
157
+ `DynamicOptic` values:
158
+
159
+ ```scala
160
+ import zio.blocks.schema._
161
+
162
+ // Equivalent builder vs interpolator
163
+ val builderPath : DynamicOptic = DynamicOptic.root.field("users").at(0).field("name")
164
+ val interpolatorPath: DynamicOptic = p".users[0].name"
165
+ // Both produce the same DynamicOptic
166
+
167
+ // Wildcards
168
+ val allEmails = p".users[*].email"
169
+
170
+ // Map access
171
+ val host = p""".config{"host"}"""
172
+
173
+ // Variant cases
174
+ val success = p".result<Success>.value"
175
+
176
+ // Complex path
177
+ val complex = p""".groups[*].members[0].contacts{"email"}"""
178
+ ```
179
+
180
+ See [Path Interpolator](../path-interpolator.md) for the full syntax reference.
181
+
182
+ ### From Typed Optics
183
+
184
+ Every typed `Optic[S, A]` can be converted to a `DynamicOptic` via `toDynamic`:
185
+
186
+ ```scala
187
+ import zio.blocks.schema._
188
+
189
+ case class Address(street: String, city: String)
190
+
191
+ object Address {
192
+ implicit val schema: Schema[Address] = Schema.derived
193
+ }
194
+
195
+ case class Person(name: String, address: Address)
196
+
197
+ object Person extends CompanionOptics[Person] {
198
+ implicit val schema: Schema[Person] = Schema.derived
199
+ val name: Lens[Person, String] = optic(_.name)
200
+ val street: Lens[Person, String] = optic(_.address.street)
201
+ }
202
+
203
+ val dynamicName: DynamicOptic = Person.name.toDynamic
204
+ // toString: .name
205
+
206
+ val dynamicStreet: DynamicOptic = Person.street.toDynamic
207
+ // toString: .address.street
208
+ ```
209
+
210
+ See [Optics](./optics.md) for more on typed optics.
211
+
212
+ ## Operations
213
+
214
+ ### Composition
215
+
216
+ `DynamicOptic` values compose via the `apply` method, which concatenates their node sequences:
217
+
218
+ ```scala
219
+ import zio.blocks.schema._
220
+
221
+ val users = DynamicOptic.root.field("users")
222
+ val first = DynamicOptic.root.at(0)
223
+ val name = DynamicOptic.root.field("name")
224
+
225
+ // Compose three paths into one
226
+ val fullPath = users(first)(name)
227
+ // toString: .users[0].name
228
+
229
+ // Compose with pre-built singletons
230
+ val allUserNames = users(DynamicOptic.elements)(name)
231
+ // toString: .users[*].name
232
+
233
+ // Compose with interpolator-built paths
234
+ val emails = users(p"[*].email")
235
+ // toString: .users[*].email
236
+ ```
237
+
238
+ ### DynamicValue Operations
239
+
240
+ `DynamicOptic` is the path argument for all `DynamicValue` operations: `get(path)` for retrieval (with `.one` or `.toChunk`), `modify(path)(f)` for transformation, `set(path, value)` for replacement, `delete(path)` for removal, and `insert(path, value)` for addition. By default, the mutating operations (`modify`, `set`, `delete`, `insert`) are lenient and return the original value unchanged if the path doesn't resolve, whereas `get(path)` yields a failing `DynamicValueSelection` on a missing path (though calling `.toChunk` on it will produce an empty chunk). Each operation also has a strict `*OrFail` variant returning `Either[SchemaError, DynamicValue]` with error details on failure.
241
+
242
+ For example, the `DynamicValue#get` method uses `DynamicOptic` to navigate and extract values:
243
+
244
+ ```scala
245
+ import zio.blocks.schema._
246
+
247
+ val data = DynamicValue.Record(
248
+ "users" -> DynamicValue.Sequence(
249
+ DynamicValue.Record("name" -> DynamicValue.string("Alice"), "age" -> DynamicValue.int(30)),
250
+ DynamicValue.Record("name" -> DynamicValue.string("Bob"), "age" -> DynamicValue.int(25))
251
+ )
252
+ )
253
+
254
+ // Get a single value
255
+ val firstName = data.get(p".users[0].name").one
256
+ // Right(DynamicValue.Primitive(PrimitiveValue.String("Alice")))
257
+
258
+ // Get all matching values (wildcard)
259
+ val allNames = data.get(p".users[*].name").toChunk
260
+ // Chunk(DynamicValue.string("Alice"), DynamicValue.string("Bob"))
261
+ ```
262
+
263
+ ### Schema & Reflect Operations
264
+
265
+ `DynamicOptic` can navigate schema structures, not just values. This is useful for schema introspection and
266
+ metaprogramming.
267
+
268
+ 1. The `Schema#get` method takes a `DynamicOptic` path and returns the `Reflect` for the nested component at that path, if it exists. This allows you to programmatically explore the structure of a schema:
269
+
270
+ ```scala
271
+ import zio.blocks.schema._
272
+
273
+ case class Address(street: String, city: String)
274
+
275
+ object Address {
276
+ implicit val schema: Schema[Address] = Schema.derived
277
+ }
278
+
279
+ case class Person(name: String, address: Address)
280
+
281
+ object Person {
282
+ implicit val schema: Schema[Person] = Schema.derived
283
+ }
284
+
285
+ // Get the Reflect for the "street" field inside Person
286
+ val streetReflect: Option[Reflect.Bound[?]] =
287
+ Schema[Person].get(p".address.street")
288
+ ```
289
+
290
+ 2. The `DynamicSchema#get` method works similarly, allowing you to navigate a `DynamicSchema` structure:
291
+
292
+ ```scala
293
+ import zio.blocks.schema._
294
+
295
+ case class Person(name: String, age: Int)
296
+
297
+ object Person {
298
+ implicit val schema: Schema[Person] = Schema.derived
299
+ }
300
+
301
+ val dynSchema = Schema[Person].toDynamicSchema
302
+
303
+ val nameReflect: Option[Reflect.Unbound[_]] =
304
+ dynSchema.get(p".name")
305
+ ```
306
+
307
+ 3. By applying a `DynamicOptic` directly to a `Reflect` value, you can navigate the reflected structure of a type:
308
+
309
+ ```scala
310
+ import zio.blocks.schema._
311
+
312
+ case class Person(name: String, age: Int)
313
+
314
+ object Person {
315
+ implicit val schema: Schema[Person] = Schema.derived
316
+ }
317
+
318
+ val reflect = Schema[Person].reflect
319
+ val path = p".name"
320
+
321
+ // Using the DynamicOptic's apply method
322
+ val result: Option[Reflect[?, ?]] = path(reflect)
323
+ ```
324
+
325
+ ## Failure Path in OpticCheck
326
+
327
+ Typed optics use `DynamicOptic` internally for error reporting. When a typed optic operation fails (e.g., a `Prism` encounters the wrong case), the error includes the `DynamicOptic` path to pinpoint exactly where the failure occurred.
328
+
329
+ ```
330
+ OpticCheck(errors: ::[Single])
331
+ └── Single (sealed trait)
332
+ ├── Error (sealed trait)
333
+ │ ├── UnexpectedCase — prism matched wrong variant case
334
+ │ └── WrappingError — wrapper conversion failed
335
+ └── Warning (sealed trait)
336
+ ├── EmptySequence — traversal over empty sequence
337
+ ├── SequenceIndexOutOfBounds — index beyond sequence length
338
+ ├── MissingKey — map key not found
339
+ └── EmptyMap — traversal over empty map
340
+ ```
341
+
342
+ Every `OpticCheck.Single` carries two `DynamicOptic` paths:
343
+
344
+ - **`full`** — The complete optic path that was being evaluated
345
+ - **`prefix`** — The path up to the point where the error occurred
346
+
347
+ Error messages use `toScalaString` for human-readable output:
348
+
349
+ ```
350
+ During attempted access at .when[Email].subject,
351
+ encountered an unexpected case at .when[Email]:
352
+ expected Email, but got Push
353
+ ```
354
+
355
+ ## Path String Syntax
356
+
357
+ `DynamicOptic` provides two string formats for different contexts:
358
+
359
+ - **`toString`** — Compact path syntax matching the [`p"..."` interpolator](../path-interpolator.md) format
360
+ - **`toScalaString`** — Scala method call syntax used in error messages
361
+
362
+ | Node | `toString` | `toScalaString` |
363
+ |---------------------------|--------------|---------------------|
364
+ | `Field("name")` | `.name` | `.name` |
365
+ | `Case("Email")` | `<Email>` | `.when[Email]` |
366
+ | `AtIndex(0)` | `[0]` | `.at(0)` |
367
+ | `AtIndices(Seq(0, 2))` | `[0,2]` | `.atIndices(0, 2)` |
368
+ | `AtMapKey(string "host")` | `{"host"}` | `.atKey("host")` |
369
+ | `AtMapKey(int 42)` | `{42}` | `.atKey(42)` |
370
+ | `AtMapKey(bool true)` | `{true}` | `.atKey(true)` |
371
+ | `AtMapKey(char 'a')` | `{'a'}` | `.atKey('a')` |
372
+ | `AtMapKeys(strings)` | `{"a", "b"}` | `.atKeys("a", "b")` |
373
+ | `Elements` | `[*]` | `.each` |
374
+ | `MapKeys` | `{*:}` | `.eachKey` |
375
+ | `MapValues` | `{*}` | `.eachValue` |
376
+ | `Wrapped` | `.~` | `.wrapped` |
377
+ | root | `.` | `.` |
378
+
379
+ ## Serialization
380
+
381
+ `DynamicOptic` has an implicit `Schema[DynamicOptic]` defined in its companion object, which means it can be serialized and deserialized just like any other schema-equipped type. This enables storing optic paths in databases, sending them over the wire, or including them in configuration files:
382
+
383
+ ```scala
384
+ import zio.blocks.schema._
385
+
386
+ // Schema[DynamicOptic] is available implicitly
387
+ val opticSchema: Schema[DynamicOptic] = Schema[DynamicOptic]
388
+
389
+ // Convert to/from DynamicValue for serialization
390
+ val path = p".users[0].name"
391
+ val serialized: DynamicValue = opticSchema.toDynamicValue(path)
392
+ ```
@@ -1,11 +1,78 @@
1
1
  ---
2
2
  id: formats
3
3
  title: "Serialization Formats"
4
+ sidebar_label: "Formats"
4
5
  ---
5
6
 
6
7
  ZIO Blocks Schema provides automatic codec derivation for multiple serialization formats. Once you have a `Schema[A]` for your data type, you can derive codecs for any supported format using the unified `Schema.derive(Format)` pattern.
7
8
 
8
- ## Overview: Codec Derivation System
9
+ A `Format` is an abstraction that bundles together everything needed to serialize and deserialize data in a specific format (JSON, Avro, Protobuf, etc.).
10
+
11
+ ## Overview
12
+
13
+ Each format defines the types of input for decoding and output for encoding, as well as the typeclass used as a codec for that format. Each format contains a `Deriver` corresponding to its specific MIME type, which is used to derive codecs from schemas:
14
+
15
+ ```scala
16
+ trait Format {
17
+ type DecodeInput
18
+ type EncodeOutput
19
+ type TypeClass[A] <: Codec[DecodeInput, EncodeOutput, A]
20
+ def mimeType: String
21
+ def deriver: Deriver[TypeClass]
22
+ }
23
+ ```
24
+
25
+ It unifies all metadata related to serialization formats, such as MIME type and codec deriver, in a single place. This allows for a consistent API across different formats when deriving codecs from schemas. Having MIME type information helps with runtime content negotiation and format routing, for example in HTTP servers or message queues.
26
+
27
+ That is, you can easily call [`Schema[A].derive(format)`](./type-class-derivation.md#using-the-deriver-to-derive-type-class-instances) for any format that implements the `Format` trait, and receive a codec that can encode and decode values of type `A` according to the rules of that format.
28
+
29
+ Formats are categorized into `BinaryFormat` and `TextFormat`, which specify the types of input and output for encoding and decoding:
30
+
31
+ ```scala
32
+ sealed trait Format
33
+ abstract class BinaryFormat[...](...) extends Format { ... }
34
+ abstract class TextFormat[...](...) extends Format { ... }
35
+ ```
36
+
37
+ For example, the `JsonFormat` is a `BinaryFormat` that represents a JSON binary format, where the input for decoding is `ByteBuffer` and the output for encoding is also `ByteBuffer`, the MIME type is `application/json`, and the deriver for generating codecs from schemas is `JsonBinaryCodecDeriver`:
38
+
39
+ ```scala
40
+ object JsonFormat extends BinaryFormat("application/json", JsonBinaryCodecDeriver)
41
+ ```
42
+
43
+ ## Built-in Formats
44
+
45
+ Here's a summary of the formats currently supported by ZIO Blocks. Each format provides a `BinaryFormat` object that can be passed to `derive`:
46
+
47
+ | Format Object | Codec Type | MIME Type | Module |
48
+ |---------------------|-----------------------------|-----------------------|---------------------------------|
49
+ | `JsonFormat` | `JsonBinaryCodec[A]` | `application/json` | `zio-blocks-schema` |
50
+ | `ToonFormat` | `ToonBinaryCodec[A]` | `text/toon` | `zio-blocks-schema-toon` |
51
+ | `MessagePackFormat` | `MessagePackBinaryCodec[A]` | `application/msgpack` | `zio-blocks-schema-messagepack` |
52
+ | `AvroFormat` | `AvroBinaryCodec[A]` | `application/avro` | `zio-blocks-schema-avro` |
53
+ | `ThriftFormat` | `ThriftBinaryCodec[A]` | `application/thrift` | `zio-blocks-schema-thrift` |
54
+
55
+ ## Defining a Custom Format
56
+
57
+ To add a new serialization format, define a `BinaryFormat` (or `TextFormat`) singleton with a custom `Deriver`:
58
+
59
+ ```scala
60
+ import zio.blocks.schema.codec.{BinaryCodec, BinaryFormat}
61
+ import zio.blocks.schema.derive.Deriver
62
+
63
+ // 1. Define your codec base class
64
+ abstract class MyCodec[A] extends BinaryCodec[A]
65
+
66
+ // 2. Implement a Deriver[MyCodec] (see Type-class Derivation docs)
67
+ // val myDeriver: Deriver[MyCodec] = ...
68
+
69
+ // 3. Create the format singleton
70
+ // object MyFormat extends BinaryFormat[MyCodec]("application/x-myformat", myDeriver)
71
+ ```
72
+
73
+ For details on implementing a `Deriver`, see [Type-class Derivation](./type-class-derivation.md).
74
+
75
+ ## Codec Derivation System
9
76
 
10
77
  All serialization formats in ZIO Blocks follow the same pattern: given a `Schema[A]`, you derive a codec by calling `derive` with a format object:
11
78
 
@@ -29,17 +96,6 @@ val bytes: Array[Byte] = codec.encode(Person("Alice", 30))
29
96
  val result: Either[SchemaError, Person] = codec.decode(bytes)
30
97
  ```
31
98
 
32
- Each format provides a `BinaryFormat` object that can be passed to `derive`:
33
-
34
- | Format | Object | MIME Type | Platform Support |
35
- |--------|--------|-----------|------------------|
36
- | JSON | `JsonFormat` | `application/json` | JVM, JS |
37
- | TOON | `ToonFormat` | `text/toon` | JVM, JS |
38
- | MessagePack | `MessagePackFormat` | `application/msgpack` | JVM, JS |
39
- | Avro | `AvroFormat` | `application/avro` | JVM only |
40
- | Thrift | `ThriftFormat` | `application/thrift` | JVM only |
41
- | BSON | `BsonSchemaCodec` | Binary | JVM only |
42
-
43
99
  ## JSON Format
44
100
 
45
101
  JSON format is the most commonly used text-based serialization format. See the dedicated [JSON documentation](json.md) for comprehensive coverage of the `Json` ADT, navigation, and transformation features.
@@ -159,6 +159,7 @@ val evenNumber = JsonSchema.integer(
159
159
  Create schemas for array validation:
160
160
 
161
161
  ```scala
162
+ import zio.blocks.chunk.NonEmptyChunk
162
163
  import zio.blocks.schema.json.{JsonSchema, JsonSchemaType, NonNegativeInt}
163
164
 
164
165
  // Array of strings
@@ -181,9 +182,9 @@ val uniqueNumbers = JsonSchema.array(
181
182
 
182
183
  // Tuple-like array with prefixItems
183
184
  val point2D = JsonSchema.array(
184
- prefixItems = Some(new ::(
185
+ prefixItems = Some(NonEmptyChunk(
185
186
  JsonSchema.ofType(JsonSchemaType.Number),
186
- JsonSchema.ofType(JsonSchemaType.Number) :: Nil
187
+ JsonSchema.ofType(JsonSchemaType.Number)
187
188
  ))
188
189
  )
189
190
  ```
@@ -219,15 +220,17 @@ val strictPerson = JsonSchema.obj(
219
220
  ### Enum and Const
220
221
 
221
222
  ```scala
223
+ import zio.blocks.chunk.NonEmptyChunk
222
224
  import zio.blocks.schema.json.{JsonSchema, Json}
223
225
 
224
226
  // Enum of string values
225
- val status = JsonSchema.enumOfStrings(new ::("pending", "active" :: "completed" :: Nil))
227
+ val status = JsonSchema.enumOfStrings(NonEmptyChunk("pending", "active", "completed"))
226
228
 
227
229
  // Enum of mixed values
228
- val mixed = JsonSchema.enumOf(new ::(
230
+ val mixed = JsonSchema.enumOf(NonEmptyChunk(
229
231
  Json.String("auto"),
230
- Json.Number(0) :: Json.Boolean(true) :: Nil
232
+ Json.Number(0),
233
+ Json.Boolean(true)
231
234
  ))
232
235
 
233
236
  // Constant value
@@ -467,8 +470,8 @@ Format validation is enabled by default. Use `ValidationOptions.annotationOnly`
467
470
  JSON Schema 2020-12 introduces `unevaluatedProperties` and `unevaluatedItems` for validating properties/items not matched by any applicator keyword:
468
471
 
469
472
  ```scala
473
+ import zio.blocks.chunk.{ChunkMap, NonEmptyChunk}
470
474
  import zio.blocks.schema.json.{JsonSchema, JsonSchemaType}
471
- import zio.blocks.chunk.ChunkMap
472
475
 
473
476
  // Reject any properties not defined in properties or patternProperties
474
477
  val strictObject = JsonSchema.Object(
@@ -480,9 +483,9 @@ val strictObject = JsonSchema.Object(
480
483
 
481
484
  // Reject extra array items not matched by prefixItems or items
482
485
  val strictArray = JsonSchema.Object(
483
- prefixItems = Some(new ::(
486
+ prefixItems = Some(NonEmptyChunk(
484
487
  JsonSchema.ofType(JsonSchemaType.String),
485
- JsonSchema.ofType(JsonSchemaType.Number) :: Nil
488
+ JsonSchema.ofType(JsonSchemaType.Number)
486
489
  )),
487
490
  unevaluatedItems = Some(JsonSchema.False)
488
491
  )
@@ -555,8 +558,8 @@ The implementation passes **817 of 844 tests** (97%+) from the official JSON Sch
555
558
  ## Complete Example
556
559
 
557
560
  ```scala
561
+ import zio.blocks.chunk.{ChunkMap, NonEmptyChunk}
558
562
  import zio.blocks.schema.json._
559
- import zio.blocks.chunk.ChunkMap
560
563
 
561
564
  // Define a complex schema
562
565
  val userSchema = JsonSchema.obj(
@@ -573,7 +576,7 @@ val userSchema = JsonSchema.obj(
573
576
  ),
574
577
  "roles" -> JsonSchema.array(
575
578
  items = Some(JsonSchema.enumOfStrings(
576
- new ::("admin", "user" :: "guest" :: Nil)
579
+ NonEmptyChunk("admin", "user", "guest")
577
580
  )),
578
581
  minItems = Some(NonNegativeInt.literal(1)),
579
582
  uniqueItems = Some(true)