@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,626 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: json-schema
|
|
3
|
+
title: "JSON Schema"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`JsonSchema` provides first-class support for [JSON Schema 2020-12](https://json-schema.org/specification-links.html#2020-12) in ZIO Blocks. It enables parsing, construction, validation, and serialization of JSON Schemas as native Scala values.
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
The `JsonSchema` type is a sealed ADT representing all valid JSON Schema documents:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
JsonSchema
|
|
14
|
+
├── JsonSchema.True (accepts all values - equivalent to {})
|
|
15
|
+
├── JsonSchema.False (rejects all values - equivalent to {"not": {}})
|
|
16
|
+
└── JsonSchema.Object (full schema with all keywords)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Key features:
|
|
20
|
+
|
|
21
|
+
- **Full JSON Schema 2020-12 support** - All standard vocabularies (core, applicator, validation, format, meta-data)
|
|
22
|
+
- **Type-safe construction** - Smart constructors and builder pattern
|
|
23
|
+
- **Validation** - Validate JSON values against schemas with detailed error messages
|
|
24
|
+
- **Round-trip serialization** - Parse from JSON and serialize back without loss
|
|
25
|
+
- **Combinators** - Compose schemas with `&&` (allOf), `||` (anyOf), `!` (not)
|
|
26
|
+
- **817 of 844 official tests passing** (97%+)
|
|
27
|
+
|
|
28
|
+
## Deriving JSON Schema from Schema
|
|
29
|
+
|
|
30
|
+
The most common use case is deriving a JSON Schema from an existing `Schema[A]`.
|
|
31
|
+
|
|
32
|
+
### Basic Derivation
|
|
33
|
+
|
|
34
|
+
```scala mdoc:compile-only
|
|
35
|
+
import zio.blocks.schema._
|
|
36
|
+
import zio.blocks.schema.json._
|
|
37
|
+
|
|
38
|
+
case class Person(name: String, age: Int)
|
|
39
|
+
object Person {
|
|
40
|
+
implicit val schema: Schema[Person] = Schema.derived
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Get JSON Schema directly from Schema
|
|
44
|
+
val jsonSchema: JsonSchema = Schema[Person].toJsonSchema
|
|
45
|
+
|
|
46
|
+
// The derived schema validates JSON values
|
|
47
|
+
val valid = Json.Object("name" -> Json.String("Alice"), "age" -> Json.Number(30))
|
|
48
|
+
val invalid = Json.Object("name" -> Json.Number(123))
|
|
49
|
+
|
|
50
|
+
jsonSchema.conforms(valid) // true
|
|
51
|
+
jsonSchema.conforms(invalid) // false
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Through JsonBinaryCodec
|
|
55
|
+
|
|
56
|
+
For more control, derive through `JsonBinaryCodec`:
|
|
57
|
+
|
|
58
|
+
```scala mdoc:compile-only
|
|
59
|
+
import zio.blocks.schema._
|
|
60
|
+
import zio.blocks.schema.json._
|
|
61
|
+
|
|
62
|
+
case class User(email: String, active: Boolean)
|
|
63
|
+
object User {
|
|
64
|
+
implicit val schema: Schema[User] = Schema.derived
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Derive codec first, then get JSON Schema
|
|
68
|
+
val codec = Schema[User].derive(JsonFormat)
|
|
69
|
+
val jsonSchema = codec.toJsonSchema
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Creating Schemas
|
|
73
|
+
|
|
74
|
+
### Boolean Schemas
|
|
75
|
+
|
|
76
|
+
The simplest schemas accept or reject all values:
|
|
77
|
+
|
|
78
|
+
```scala mdoc:compile-only
|
|
79
|
+
import zio.blocks.schema.json.JsonSchema
|
|
80
|
+
|
|
81
|
+
// Accepts any valid JSON value
|
|
82
|
+
val acceptAll = JsonSchema.True
|
|
83
|
+
|
|
84
|
+
// Rejects all JSON values
|
|
85
|
+
val rejectAll = JsonSchema.False
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Type Schemas
|
|
89
|
+
|
|
90
|
+
Create schemas that validate specific JSON types:
|
|
91
|
+
|
|
92
|
+
```scala mdoc:compile-only
|
|
93
|
+
import zio.blocks.schema.json.{JsonSchema, JsonSchemaType}
|
|
94
|
+
|
|
95
|
+
// Single type
|
|
96
|
+
val stringSchema = JsonSchema.ofType(JsonSchemaType.String)
|
|
97
|
+
val numberSchema = JsonSchema.ofType(JsonSchemaType.Number)
|
|
98
|
+
val integerSchema = JsonSchema.ofType(JsonSchemaType.Integer)
|
|
99
|
+
val booleanSchema = JsonSchema.ofType(JsonSchemaType.Boolean)
|
|
100
|
+
val arraySchema = JsonSchema.ofType(JsonSchemaType.Array)
|
|
101
|
+
val objectSchema = JsonSchema.ofType(JsonSchemaType.Object)
|
|
102
|
+
val nullSchema = JsonSchema.ofType(JsonSchemaType.Null)
|
|
103
|
+
|
|
104
|
+
// Convenience aliases
|
|
105
|
+
val isNull = JsonSchema.nullSchema
|
|
106
|
+
val isBoolean = JsonSchema.boolean
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### String Schemas
|
|
110
|
+
|
|
111
|
+
Create schemas for string validation:
|
|
112
|
+
|
|
113
|
+
```scala mdoc:compile-only
|
|
114
|
+
import zio.blocks.schema.json.{JsonSchema, NonNegativeInt, RegexPattern}
|
|
115
|
+
|
|
116
|
+
// String with length constraints (compile-time validated literals)
|
|
117
|
+
val username = JsonSchema.string(
|
|
118
|
+
NonNegativeInt.literal(3),
|
|
119
|
+
NonNegativeInt.literal(20)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
// String with pattern
|
|
123
|
+
val hexColor = JsonSchema.string(
|
|
124
|
+
pattern = RegexPattern.unsafe("^#[0-9a-fA-F]{6}$")
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
// String with format
|
|
128
|
+
val email = JsonSchema.string(format = Some("email"))
|
|
129
|
+
val dateTime = JsonSchema.string(format = Some("date-time"))
|
|
130
|
+
val uuid = JsonSchema.string(format = Some("uuid"))
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Numeric Schemas
|
|
134
|
+
|
|
135
|
+
Create schemas for number validation:
|
|
136
|
+
|
|
137
|
+
```scala mdoc:compile-only
|
|
138
|
+
import zio.blocks.schema.json.{JsonSchema, PositiveNumber}
|
|
139
|
+
|
|
140
|
+
// Number with range
|
|
141
|
+
val percentage = JsonSchema.number(
|
|
142
|
+
minimum = Some(BigDecimal(0)),
|
|
143
|
+
maximum = Some(BigDecimal(100))
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
// Integer with exclusive bounds
|
|
147
|
+
val positiveInt = JsonSchema.integer(
|
|
148
|
+
exclusiveMinimum = Some(BigDecimal(0))
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
// Number divisible by a value
|
|
152
|
+
val evenNumber = JsonSchema.integer(
|
|
153
|
+
multipleOf = PositiveNumber.fromInt(2)
|
|
154
|
+
)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Array Schemas
|
|
158
|
+
|
|
159
|
+
Create schemas for array validation:
|
|
160
|
+
|
|
161
|
+
```scala mdoc:compile-only
|
|
162
|
+
import zio.blocks.schema.json.{JsonSchema, JsonSchemaType, NonNegativeInt}
|
|
163
|
+
|
|
164
|
+
// Array of strings
|
|
165
|
+
val stringArray = JsonSchema.array(
|
|
166
|
+
items = Some(JsonSchema.ofType(JsonSchemaType.String))
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
// Array with length constraints
|
|
170
|
+
val shortList = JsonSchema.array(
|
|
171
|
+
JsonSchema.ofType(JsonSchemaType.Number),
|
|
172
|
+
NonNegativeInt.literal(1),
|
|
173
|
+
NonNegativeInt.literal(5)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
// Array with unique items
|
|
177
|
+
val uniqueNumbers = JsonSchema.array(
|
|
178
|
+
items = Some(JsonSchema.ofType(JsonSchemaType.Number)),
|
|
179
|
+
uniqueItems = Some(true)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
// Tuple-like array with prefixItems
|
|
183
|
+
val point2D = JsonSchema.array(
|
|
184
|
+
prefixItems = Some(new ::(
|
|
185
|
+
JsonSchema.ofType(JsonSchemaType.Number),
|
|
186
|
+
JsonSchema.ofType(JsonSchemaType.Number) :: Nil
|
|
187
|
+
))
|
|
188
|
+
)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Object Schemas
|
|
192
|
+
|
|
193
|
+
Create schemas for object validation:
|
|
194
|
+
|
|
195
|
+
```scala mdoc:compile-only
|
|
196
|
+
import zio.blocks.schema.json.{JsonSchema, JsonSchemaType}
|
|
197
|
+
import zio.blocks.chunk.ChunkMap
|
|
198
|
+
|
|
199
|
+
// Object with properties
|
|
200
|
+
val person = JsonSchema.obj(
|
|
201
|
+
properties = Some(ChunkMap(
|
|
202
|
+
"name" -> JsonSchema.ofType(JsonSchemaType.String),
|
|
203
|
+
"age" -> JsonSchema.ofType(JsonSchemaType.Integer)
|
|
204
|
+
)),
|
|
205
|
+
required = Some(Set("name"))
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
// Object with no additional properties
|
|
209
|
+
val strictPerson = JsonSchema.obj(
|
|
210
|
+
properties = Some(ChunkMap(
|
|
211
|
+
"name" -> JsonSchema.ofType(JsonSchemaType.String),
|
|
212
|
+
"age" -> JsonSchema.ofType(JsonSchemaType.Integer)
|
|
213
|
+
)),
|
|
214
|
+
required = Some(Set("name")),
|
|
215
|
+
additionalProperties = Some(JsonSchema.False)
|
|
216
|
+
)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Enum and Const
|
|
220
|
+
|
|
221
|
+
```scala mdoc:compile-only
|
|
222
|
+
import zio.blocks.schema.json.{JsonSchema, Json}
|
|
223
|
+
|
|
224
|
+
// Enum of string values
|
|
225
|
+
val status = JsonSchema.enumOfStrings(new ::("pending", "active" :: "completed" :: Nil))
|
|
226
|
+
|
|
227
|
+
// Enum of mixed values
|
|
228
|
+
val mixed = JsonSchema.enumOf(new ::(
|
|
229
|
+
Json.String("auto"),
|
|
230
|
+
Json.Number(0) :: Json.Boolean(true) :: Nil
|
|
231
|
+
))
|
|
232
|
+
|
|
233
|
+
// Constant value
|
|
234
|
+
val alwaysTrue = JsonSchema.constOf(Json.Boolean(true))
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Schema Combinators
|
|
238
|
+
|
|
239
|
+
### Logical Composition
|
|
240
|
+
|
|
241
|
+
Combine schemas using logical operators:
|
|
242
|
+
|
|
243
|
+
```scala mdoc:compile-only
|
|
244
|
+
import zio.blocks.schema.json.{JsonSchema, JsonSchemaType}
|
|
245
|
+
|
|
246
|
+
val stringSchema = JsonSchema.ofType(JsonSchemaType.String)
|
|
247
|
+
val numberSchema = JsonSchema.ofType(JsonSchemaType.Number)
|
|
248
|
+
val nullSchema = JsonSchema.ofType(JsonSchemaType.Null)
|
|
249
|
+
|
|
250
|
+
// allOf - must match all schemas
|
|
251
|
+
val stringAndNotEmpty = stringSchema && JsonSchema.string(
|
|
252
|
+
minLength = Some(zio.blocks.schema.json.NonNegativeInt.literal(1))
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
// anyOf - must match at least one schema
|
|
256
|
+
val stringOrNumber = stringSchema || numberSchema
|
|
257
|
+
|
|
258
|
+
// not - must not match the schema
|
|
259
|
+
val notNull = !nullSchema
|
|
260
|
+
|
|
261
|
+
// Combining operators
|
|
262
|
+
val nullableString = stringSchema || nullSchema
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Nullable Schemas
|
|
266
|
+
|
|
267
|
+
Make any schema nullable:
|
|
268
|
+
|
|
269
|
+
```scala mdoc:compile-only
|
|
270
|
+
import zio.blocks.schema.json.{JsonSchema, JsonSchemaType}
|
|
271
|
+
|
|
272
|
+
val stringSchema = JsonSchema.ofType(JsonSchemaType.String)
|
|
273
|
+
|
|
274
|
+
// Accepts string or null
|
|
275
|
+
val nullableString = stringSchema.withNullable
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Conditional Schemas
|
|
279
|
+
|
|
280
|
+
### if/then/else
|
|
281
|
+
|
|
282
|
+
Apply different schemas based on conditions:
|
|
283
|
+
|
|
284
|
+
```scala mdoc:compile-only
|
|
285
|
+
import zio.blocks.schema.json.{JsonSchema, JsonSchemaType, NonNegativeInt}
|
|
286
|
+
|
|
287
|
+
// If type is string, require minLength
|
|
288
|
+
val conditionalSchema = JsonSchema.Object(
|
|
289
|
+
`if` = Some(JsonSchema.ofType(JsonSchemaType.String)),
|
|
290
|
+
`then` = Some(JsonSchema.string(minLength = Some(NonNegativeInt.literal(1)))),
|
|
291
|
+
`else` = Some(JsonSchema.True)
|
|
292
|
+
)
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Dependent Schemas
|
|
296
|
+
|
|
297
|
+
Apply schemas when properties are present:
|
|
298
|
+
|
|
299
|
+
```scala mdoc:compile-only
|
|
300
|
+
import zio.blocks.schema.json.{JsonSchema, JsonSchemaType}
|
|
301
|
+
import zio.blocks.chunk.ChunkMap
|
|
302
|
+
|
|
303
|
+
// If "credit_card" exists, require "billing_address"
|
|
304
|
+
val paymentSchema = JsonSchema.Object(
|
|
305
|
+
properties = Some(ChunkMap(
|
|
306
|
+
"credit_card" -> JsonSchema.ofType(JsonSchemaType.String),
|
|
307
|
+
"billing_address" -> JsonSchema.ofType(JsonSchemaType.String)
|
|
308
|
+
)),
|
|
309
|
+
dependentRequired = Some(ChunkMap(
|
|
310
|
+
"credit_card" -> Set("billing_address")
|
|
311
|
+
))
|
|
312
|
+
)
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Validation
|
|
316
|
+
|
|
317
|
+
### Basic Validation
|
|
318
|
+
|
|
319
|
+
```scala mdoc:compile-only
|
|
320
|
+
import zio.blocks.schema.json.{JsonSchema, Json, JsonSchemaType}
|
|
321
|
+
import zio.blocks.chunk.ChunkMap
|
|
322
|
+
|
|
323
|
+
val schema = JsonSchema.obj(
|
|
324
|
+
properties = Some(ChunkMap(
|
|
325
|
+
"name" -> JsonSchema.ofType(JsonSchemaType.String),
|
|
326
|
+
"age" -> JsonSchema.integer(minimum = Some(BigDecimal(0)))
|
|
327
|
+
)),
|
|
328
|
+
required = Some(Set("name"))
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
val validJson = Json.Object(
|
|
332
|
+
"name" -> Json.String("Alice"),
|
|
333
|
+
"age" -> Json.Number(30)
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
val invalidJson = Json.Object(
|
|
337
|
+
"age" -> Json.Number(-5)
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
// Using check() - returns Option[SchemaError]
|
|
341
|
+
schema.check(validJson) // None (valid)
|
|
342
|
+
schema.check(invalidJson) // Some(SchemaError(...))
|
|
343
|
+
|
|
344
|
+
// Using conforms() - returns Boolean
|
|
345
|
+
schema.conforms(validJson) // true
|
|
346
|
+
schema.conforms(invalidJson) // false
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Validation Options
|
|
350
|
+
|
|
351
|
+
Control validation behavior:
|
|
352
|
+
|
|
353
|
+
```scala mdoc:compile-only
|
|
354
|
+
import zio.blocks.schema.json.{JsonSchema, Json, ValidationOptions}
|
|
355
|
+
|
|
356
|
+
val schema = JsonSchema.string(format = Some("email"))
|
|
357
|
+
val value = Json.String("not-an-email")
|
|
358
|
+
|
|
359
|
+
// With format validation (default)
|
|
360
|
+
val strictOptions = ValidationOptions.formatAssertion
|
|
361
|
+
schema.check(value, strictOptions) // Some(error)
|
|
362
|
+
|
|
363
|
+
// Without format validation (format as annotation only)
|
|
364
|
+
val lenientOptions = ValidationOptions.annotationOnly
|
|
365
|
+
schema.check(value, lenientOptions) // None
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### Error Messages
|
|
369
|
+
|
|
370
|
+
Validation errors include path information:
|
|
371
|
+
|
|
372
|
+
```scala mdoc:compile-only
|
|
373
|
+
import zio.blocks.schema.json.{JsonSchema, Json, JsonSchemaType}
|
|
374
|
+
import zio.blocks.chunk.ChunkMap
|
|
375
|
+
|
|
376
|
+
val schema = JsonSchema.obj(
|
|
377
|
+
properties = Some(ChunkMap(
|
|
378
|
+
"users" -> JsonSchema.array(
|
|
379
|
+
items = Some(JsonSchema.obj(
|
|
380
|
+
properties = Some(ChunkMap(
|
|
381
|
+
"email" -> JsonSchema.string(format = Some("email"))
|
|
382
|
+
))
|
|
383
|
+
))
|
|
384
|
+
)
|
|
385
|
+
))
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
val invalid = Json.Object(
|
|
389
|
+
"users" -> Json.Array(
|
|
390
|
+
Json.Object("email" -> Json.String("invalid"))
|
|
391
|
+
)
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
schema.check(invalid) match {
|
|
395
|
+
case Some(err) => println(err.message)
|
|
396
|
+
// "String 'invalid' is not a valid email address"
|
|
397
|
+
case None => println("Valid")
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
## Parsing and Serialization
|
|
402
|
+
|
|
403
|
+
### Parsing from JSON
|
|
404
|
+
|
|
405
|
+
```scala mdoc:compile-only
|
|
406
|
+
import zio.blocks.schema.json.{JsonSchema, Json}
|
|
407
|
+
|
|
408
|
+
// From JSON string
|
|
409
|
+
val parsed = JsonSchema.parse("""
|
|
410
|
+
{
|
|
411
|
+
"type": "object",
|
|
412
|
+
"properties": {
|
|
413
|
+
"name": { "type": "string" }
|
|
414
|
+
},
|
|
415
|
+
"required": ["name"]
|
|
416
|
+
}
|
|
417
|
+
""")
|
|
418
|
+
|
|
419
|
+
// From Json value
|
|
420
|
+
val json = Json.Object(
|
|
421
|
+
"type" -> Json.String("string"),
|
|
422
|
+
"minLength" -> Json.Number(1)
|
|
423
|
+
)
|
|
424
|
+
val fromJson = JsonSchema.fromJson(json)
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Serializing to JSON
|
|
428
|
+
|
|
429
|
+
```scala mdoc:compile-only
|
|
430
|
+
import zio.blocks.schema.json.{JsonSchema, NonNegativeInt}
|
|
431
|
+
|
|
432
|
+
val schema = JsonSchema.string(
|
|
433
|
+
NonNegativeInt.literal(1),
|
|
434
|
+
NonNegativeInt.literal(100)
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
val json = schema.toJson
|
|
438
|
+
// {"type":"string","minLength":1,"maxLength":100}
|
|
439
|
+
|
|
440
|
+
val jsonString = json.print
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
## Format Validation
|
|
444
|
+
|
|
445
|
+
The following formats are supported for validation:
|
|
446
|
+
|
|
447
|
+
| Format | Description | Example |
|
|
448
|
+
|--------|-------------|---------|
|
|
449
|
+
| `date-time` | RFC 3339 date-time | `2024-01-15T10:30:00Z` |
|
|
450
|
+
| `date` | RFC 3339 full-date | `2024-01-15` |
|
|
451
|
+
| `time` | RFC 3339 full-time | `10:30:00Z` |
|
|
452
|
+
| `email` | Email address | `user@example.com` |
|
|
453
|
+
| `uuid` | RFC 4122 UUID | `550e8400-e29b-41d4-a716-446655440000` |
|
|
454
|
+
| `uri` | RFC 3986 URI | `https://example.com/path` |
|
|
455
|
+
| `uri-reference` | RFC 3986 URI-reference | `/path/to/resource` |
|
|
456
|
+
| `ipv4` | IPv4 address | `192.168.1.1` |
|
|
457
|
+
| `ipv6` | IPv6 address | `2001:db8::1` |
|
|
458
|
+
| `hostname` | RFC 1123 hostname | `example.com` |
|
|
459
|
+
| `regex` | ECMA-262 regex | `^[a-z]+$` |
|
|
460
|
+
| `duration` | ISO 8601 duration | `P3Y6M4DT12H30M5S` |
|
|
461
|
+
| `json-pointer` | RFC 6901 JSON Pointer | `/foo/bar/0` |
|
|
462
|
+
|
|
463
|
+
Format validation is enabled by default. Use `ValidationOptions.annotationOnly` to treat `format` as annotation only (per JSON Schema spec).
|
|
464
|
+
|
|
465
|
+
## Unevaluated Properties and Items
|
|
466
|
+
|
|
467
|
+
JSON Schema 2020-12 introduces `unevaluatedProperties` and `unevaluatedItems` for validating properties/items not matched by any applicator keyword:
|
|
468
|
+
|
|
469
|
+
```scala mdoc:compile-only
|
|
470
|
+
import zio.blocks.schema.json.{JsonSchema, JsonSchemaType}
|
|
471
|
+
import zio.blocks.chunk.ChunkMap
|
|
472
|
+
|
|
473
|
+
// Reject any properties not defined in properties or patternProperties
|
|
474
|
+
val strictObject = JsonSchema.Object(
|
|
475
|
+
properties = Some(ChunkMap(
|
|
476
|
+
"name" -> JsonSchema.ofType(JsonSchemaType.String)
|
|
477
|
+
)),
|
|
478
|
+
unevaluatedProperties = Some(JsonSchema.False)
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
// Reject extra array items not matched by prefixItems or items
|
|
482
|
+
val strictArray = JsonSchema.Object(
|
|
483
|
+
prefixItems = Some(new ::(
|
|
484
|
+
JsonSchema.ofType(JsonSchemaType.String),
|
|
485
|
+
JsonSchema.ofType(JsonSchemaType.Number) :: Nil
|
|
486
|
+
)),
|
|
487
|
+
unevaluatedItems = Some(JsonSchema.False)
|
|
488
|
+
)
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
## Schema Object Fields
|
|
492
|
+
|
|
493
|
+
`JsonSchema.Object` supports all JSON Schema 2020-12 keywords:
|
|
494
|
+
|
|
495
|
+
### Core Vocabulary
|
|
496
|
+
- `$id`, `$schema`, `$anchor`, `$dynamicAnchor`
|
|
497
|
+
- `$ref`, `$dynamicRef` (limited support - see Limitations)
|
|
498
|
+
- `$defs`, `$comment`
|
|
499
|
+
|
|
500
|
+
### Applicator Vocabulary
|
|
501
|
+
- `allOf`, `anyOf`, `oneOf`, `not`
|
|
502
|
+
- `if`, `then`, `else`
|
|
503
|
+
- `properties`, `patternProperties`, `additionalProperties`
|
|
504
|
+
- `propertyNames`, `dependentSchemas`
|
|
505
|
+
- `prefixItems`, `items`, `contains`
|
|
506
|
+
|
|
507
|
+
### Unevaluated Vocabulary
|
|
508
|
+
- `unevaluatedProperties`, `unevaluatedItems`
|
|
509
|
+
|
|
510
|
+
### Validation Vocabulary
|
|
511
|
+
- `type`, `enum`, `const`
|
|
512
|
+
- `multipleOf`, `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`
|
|
513
|
+
- `minLength`, `maxLength`, `pattern`
|
|
514
|
+
- `minItems`, `maxItems`, `uniqueItems`, `minContains`, `maxContains`
|
|
515
|
+
- `minProperties`, `maxProperties`, `required`, `dependentRequired`
|
|
516
|
+
|
|
517
|
+
### Format Vocabulary
|
|
518
|
+
- `format`
|
|
519
|
+
|
|
520
|
+
### Content Vocabulary
|
|
521
|
+
- `contentEncoding`, `contentMediaType`, `contentSchema`
|
|
522
|
+
|
|
523
|
+
### Meta-Data Vocabulary
|
|
524
|
+
- `title`, `description`, `default`, `deprecated`
|
|
525
|
+
- `readOnly`, `writeOnly`, `examples`
|
|
526
|
+
|
|
527
|
+
## Limitations
|
|
528
|
+
|
|
529
|
+
### Not Implemented
|
|
530
|
+
|
|
531
|
+
The following features require reference resolution and are **not supported**:
|
|
532
|
+
|
|
533
|
+
| Feature | Description |
|
|
534
|
+
|---------|-------------|
|
|
535
|
+
| `$ref` to external URIs | References to other files or URLs |
|
|
536
|
+
| `$dynamicRef` / `$dynamicAnchor` | Dynamic reference resolution |
|
|
537
|
+
| `$id` resolution | Base URI changing and resolution |
|
|
538
|
+
| Remote references | Fetching schemas from URLs |
|
|
539
|
+
| Recursive schemas via `$ref` | Self-referential schemas using references |
|
|
540
|
+
|
|
541
|
+
Local `$ref` within the same schema is partially supported for `#/$defs/...` references only.
|
|
542
|
+
|
|
543
|
+
### Known Edge Cases
|
|
544
|
+
|
|
545
|
+
| Case | Behavior |
|
|
546
|
+
|------|----------|
|
|
547
|
+
| Float/integer numeric equality | `1.0` is not treated as equal to `1` for `const`/`enum` |
|
|
548
|
+
| String length | Measured in codepoints, not grapheme clusters |
|
|
549
|
+
| Some `unevaluatedItems` with `contains` | Edge cases involving item evaluation tracking |
|
|
550
|
+
|
|
551
|
+
### Test Suite Compliance
|
|
552
|
+
|
|
553
|
+
The implementation passes **817 of 844 tests** (97%+) from the official JSON Schema Test Suite for draft2020-12. The remaining tests require reference resolution features listed above.
|
|
554
|
+
|
|
555
|
+
## Complete Example
|
|
556
|
+
|
|
557
|
+
```scala mdoc:compile-only
|
|
558
|
+
import zio.blocks.schema.json._
|
|
559
|
+
import zio.blocks.chunk.ChunkMap
|
|
560
|
+
|
|
561
|
+
// Define a complex schema
|
|
562
|
+
val userSchema = JsonSchema.obj(
|
|
563
|
+
properties = Some(ChunkMap(
|
|
564
|
+
"id" -> JsonSchema.string(format = Some("uuid")),
|
|
565
|
+
"email" -> JsonSchema.string(format = Some("email")),
|
|
566
|
+
"name" -> JsonSchema.string(
|
|
567
|
+
NonNegativeInt.literal(1),
|
|
568
|
+
NonNegativeInt.literal(100)
|
|
569
|
+
),
|
|
570
|
+
"age" -> JsonSchema.integer(
|
|
571
|
+
minimum = Some(BigDecimal(0)),
|
|
572
|
+
maximum = Some(BigDecimal(150))
|
|
573
|
+
),
|
|
574
|
+
"roles" -> JsonSchema.array(
|
|
575
|
+
items = Some(JsonSchema.enumOfStrings(
|
|
576
|
+
new ::("admin", "user" :: "guest" :: Nil)
|
|
577
|
+
)),
|
|
578
|
+
minItems = Some(NonNegativeInt.literal(1)),
|
|
579
|
+
uniqueItems = Some(true)
|
|
580
|
+
),
|
|
581
|
+
"metadata" -> JsonSchema.obj(
|
|
582
|
+
additionalProperties = Some(JsonSchema.ofType(JsonSchemaType.String))
|
|
583
|
+
)
|
|
584
|
+
)),
|
|
585
|
+
required = Some(Set("id", "email", "name", "roles")),
|
|
586
|
+
additionalProperties = Some(JsonSchema.False)
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
// Validate some data
|
|
590
|
+
val validUser = Json.Object(
|
|
591
|
+
"id" -> Json.String("550e8400-e29b-41d4-a716-446655440000"),
|
|
592
|
+
"email" -> Json.String("alice@example.com"),
|
|
593
|
+
"name" -> Json.String("Alice"),
|
|
594
|
+
"roles" -> Json.Array(Json.String("admin"), Json.String("user"))
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
val invalidUser = Json.Object(
|
|
598
|
+
"id" -> Json.String("not-a-uuid"),
|
|
599
|
+
"email" -> Json.String("invalid-email"),
|
|
600
|
+
"name" -> Json.String(""),
|
|
601
|
+
"roles" -> Json.Array(),
|
|
602
|
+
"extra" -> Json.String("not allowed")
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
userSchema.conforms(validUser) // true
|
|
606
|
+
userSchema.conforms(invalidUser) // false
|
|
607
|
+
|
|
608
|
+
// Get detailed errors
|
|
609
|
+
userSchema.check(invalidUser) match {
|
|
610
|
+
case Some(err) => println(err.message)
|
|
611
|
+
case None => println("Valid!")
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Serialize the schema
|
|
615
|
+
val schemaJson = userSchema.toJson.print
|
|
616
|
+
// Can be sent to other tools, stored, or shared
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
## Cross-Platform Support
|
|
620
|
+
|
|
621
|
+
`JsonSchema` works across all platforms:
|
|
622
|
+
|
|
623
|
+
- **JVM** - Full functionality
|
|
624
|
+
- **Scala.js** - Browser and Node.js
|
|
625
|
+
|
|
626
|
+
All features work identically across platforms.
|