@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,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.