@arrirpc/schema 0.45.0

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/README.md ADDED
@@ -0,0 +1,983 @@
1
+ # Arri Schema
2
+
3
+ A type builder and validation library built on top of the [Json Type Definition (RFC 8927)](https://jsontypedef.com) This library is pretty similar to [Typebox](https://github.com/sinclairzx81/typebox) except that it creates Json Type Definition (JTD) objects instead of Json Schema objects.
4
+
5
+ A lot of inspiration was taken from both [Typebox](https://github.com/sinclairzx81/typebox) and [Zod](https://github.com/colinhacks/zod) when designing this library.
6
+
7
+ ## Project Philosophy
8
+
9
+ The goals of this project are as follows:
10
+
11
+ - Portable type definitions
12
+ - High performance validation, parsing, and serialization
13
+ - Consistent error reporting for parsing and serialization errors
14
+
15
+ I am not looking to support every feature of Typescript's type system or even every possible representation of JSON. The goal is that the data models defined through this library can be used as a source of truth across multiple programming languages. Both JSON and Typescript have to be limited to accomplish this.
16
+
17
+ ### Adherence to RFC 8927
18
+
19
+ To represent the data-models in a language agnostic way this library heavily relies on JSON Type Definition (JTD). However, this library does not strictly comply with the JTD specification. The reason for this is because JTD does not support 64-bit integers. I believe sharing large integers across languages is a huge pain point especially when going to and from Javascript. For this reason alone, I have opted to break away from the JTD spec and add support for `int64` and `uint64`. So while I have no intention to break further away from the spec I am open to it if a large enough issue arises (in my view). If you use this library be aware that I'm using a superset of JTD rather than a strict spec compliant implementation.
20
+
21
+ ## Table of Contents
22
+
23
+ - [Installation](#installation)
24
+ - [Basic Example](#basic-example)
25
+ - [Supported Types](#supported-types)
26
+ - [Primitives](#primitives)
27
+ - [Enums](#enums)
28
+ - [Arrays / Lists](#arrays--lists)
29
+ - [Objects](#objects)
30
+ - [Records / Maps](#records--maps)
31
+ - [Discriminated Unions](#discriminated-unions)
32
+ - [Recursive Types](#recursive-types)
33
+ - [Modifiers](#modifiers)
34
+ - [Optional](#optional)
35
+ - [Nullable](#nullable)
36
+ - [Extend](#extend)
37
+ - [Omit](#omit)
38
+ - [Pick](#pick)
39
+ - [Partial](#partial)
40
+ - [Utilities](#utilities)
41
+ - [Validate](#validate)
42
+ - [Parse](#parse)
43
+ - [Safe Parse](#safe-parse)
44
+ - [Coerce](#coerce)
45
+ - [Safe Coerce](#safe-coerce)
46
+ - [Serialize](#serialize)
47
+ - [Errors](#errors)
48
+ - [Compiled Validators](#compiled-validators)
49
+ - [Metadata](#metadata)
50
+ - [Benchmarks](#benchmarks)
51
+ - [Development](#development)
52
+
53
+ ## Installation
54
+
55
+ ```bash
56
+ # npm
57
+ npm install @arrirpc/schema
58
+
59
+ # pnpm
60
+ pnpm install @arrirpc/schema
61
+ ```
62
+
63
+ ## Basic Example
64
+
65
+ ```ts
66
+ import { a } from "@arrirpc/schema";
67
+
68
+ const User = a.object({
69
+ id: a.string(),
70
+ name: a.string(),
71
+ });
72
+
73
+ type User = a.infer<typeof User>;
74
+
75
+ // passes and returns User
76
+ a.parse(User, `{"id": "1", "name": "John Doe"}`);
77
+ // throws error
78
+ a.parse(User, `{"id": "1", "name": null}`);
79
+
80
+ // returns true
81
+ a.validate(User, { id: "1", name: "John Doe" });
82
+ // returns false
83
+ a.validate(User, { id: "1", name: null });
84
+
85
+ // outputs valid json
86
+ a.serialize(User, { id: "1", name: "John Doe" });
87
+ ```
88
+
89
+ ## Supported Types
90
+
91
+ ### Primitives
92
+
93
+ | Arri Schema | Typescript | Json Type Definition |
94
+ | ------------- | ---------- | --------------------- |
95
+ | a.any() | any | {} |
96
+ | a.string() | string | { "type": "string" } |
97
+ | a.boolean() | boolean | {"type": "boolean"} |
98
+ | a.timestamp() | Date | {"type": "timestamp"} |
99
+ | a.float32() | number | {"type": "float32"} |
100
+ | a.float64() | number | {"type": "float64"} |
101
+ | a.int8() | number | {"type": "int8"} |
102
+ | a.int16() | number | {"type": "int16"} |
103
+ | a.int32() | number | {"type": "int32"} |
104
+ | a.int64() | BigInt | {"type": "int64"} |
105
+ | a.uint8() | number | {"type": "uint8"} |
106
+ | a.uint16() | number | {"type": "uint16"} |
107
+ | a.uint32() | number | {"type": "uint32"} |
108
+ | a.uint64() | BigInt | {"type": "uint64"} |
109
+
110
+ ### Enums
111
+
112
+ Enum schemas allow you to specify a predefine list of accepted strings
113
+
114
+ **Usage**
115
+
116
+ ```ts
117
+ const Status = a.enumerator(["ACTIVE", "INACTIVE", "UNKNOWN"]);
118
+ type Status = a.infer<typeof Status>; // "ACTIVE" | "INACTIVE" | "UNKNOWN";
119
+
120
+ a.validate(Status, "BLAH"); // false
121
+ a.validate(Status, "ACTIVE"); // true
122
+ ```
123
+
124
+ **Outputted JTD**
125
+
126
+ ```json
127
+ {
128
+ "enum": ["ACTIVE", "INACTIVE", "UNKNOWN"]
129
+ }
130
+ ```
131
+
132
+ ### Arrays / Lists
133
+
134
+ **Usage**
135
+
136
+ ```ts
137
+ const MyList = a.array(a.string());
138
+ type MyList = a.infer<typeof MyList>; // string[];
139
+
140
+ a.validate(MyList, [1, 2]); // false
141
+ a.validate(MyList, ["hello", "world"]); // true
142
+ ```
143
+
144
+ **Outputted JTD**
145
+
146
+ ```json
147
+ {
148
+ "elements": {
149
+ "type": "string"
150
+ }
151
+ }
152
+ ```
153
+
154
+ ### Objects
155
+
156
+ **Usage**
157
+
158
+ ```ts
159
+ const User = a.object({
160
+ id: a.string(),
161
+ email: a.string(),
162
+ created: a.timestamp(),
163
+ });
164
+ type User = a.infer<typeof User>; // { id: string; email: string; created: Date; }
165
+
166
+ a.validate({
167
+ id: "1",
168
+ email: "johndoe@example.com",
169
+ created: new Date(),
170
+ }); // true
171
+ a.validate({
172
+ id: "1",
173
+ email: null,
174
+ created: new Date(),
175
+ }); // false
176
+ ```
177
+
178
+ **Outputted JTD**
179
+
180
+ ```json
181
+ {
182
+ "properties": {
183
+ "id": {
184
+ "type": "string"
185
+ },
186
+ "email": {
187
+ "type": "string"
188
+ },
189
+ "created": {
190
+ "type": "timestamp"
191
+ }
192
+ },
193
+ "additionalProperties": true
194
+ }
195
+ ```
196
+
197
+ #### Strict Mode
198
+
199
+ By default @arrirpc/schema will ignore and strip out any additional properties when validating objects. If you want validation to fail when additional properties are present then modify the `additionalProperties` option.
200
+
201
+ ```ts
202
+ const UserStrict = a.object(
203
+ {
204
+ id: a.string(),
205
+ name: a.string(),
206
+ created: a.timestamp(),
207
+ },
208
+ {
209
+ additionalProperties: false,
210
+ },
211
+ );
212
+
213
+ a.parse(UserStrict, {
214
+ id: "1",
215
+ name: "johndoe",
216
+ created: new Date(),
217
+ bio: "my name is joe",
218
+ }); // fails parsing because of the additional field "bio"
219
+ ```
220
+
221
+ **Outputted JTD**
222
+
223
+ ```json
224
+ {
225
+ "properties": {
226
+ "id": {
227
+ "type": "string"
228
+ },
229
+ "email": {
230
+ "type": "string"
231
+ },
232
+ "created": {
233
+ "type": "timestamp"
234
+ }
235
+ },
236
+ "additionalProperties": false
237
+ }
238
+ ```
239
+
240
+ ### Records / Maps
241
+
242
+ **Usage**
243
+
244
+ ```ts
245
+ const R = a.record(a.boolean());
246
+ type R = a.infer<typeof R>; // Record<string, boolean>
247
+
248
+ a.validate(R, {
249
+ hello: true,
250
+ world: false,
251
+ }); // true;
252
+ a.validate(R, {
253
+ hello: "world",
254
+ }); // false;
255
+ ```
256
+
257
+ **Outputted JTD**
258
+
259
+ ```json
260
+ {
261
+ "values": {
262
+ "type": "boolean"
263
+ }
264
+ }
265
+ ```
266
+
267
+ ### Discriminated Unions
268
+
269
+ **Usage**
270
+
271
+ ```ts
272
+ const Shape = a.discriminator("type", {
273
+ RECTANGLE: a.object({
274
+ width: a.float32(),
275
+ height: a.float32(),
276
+ }),
277
+ CIRCLE: a.object({
278
+ radius: a.float32(),
279
+ }),
280
+ });
281
+ type Shape = a.infer<typeof Shape>; // { type: "RECTANGLE"; width: number; height: number; } | { type: "CIRCLE"; radius: number; }
282
+
283
+ // Infer specific sub types of the union
284
+ type ShapeTypeRectangle = a.inferSubType<Shape, "type", "RECTANGLE">; // { type "RECTANGLE"; width: number; height: number; };
285
+ type ShapeTypeCircle = a.inferSubType<Shape, "type", "CIRCLE">; // { type "CIRCLE"; radius: number; }
286
+
287
+ a.validate(Shape, {
288
+ type: "RECTANGLE",
289
+ width: 1,
290
+ height: 1.5,
291
+ }); // true
292
+ a.validate(Shape, {
293
+ type: "CIRCLE",
294
+ radius: 5,
295
+ }); // true
296
+ a.validate(Shape, {
297
+ type: "CIRCLE",
298
+ width: 1,
299
+ height: 1.5,
300
+ }); // false
301
+ ```
302
+
303
+ **Outputted JTD**
304
+
305
+ ```json
306
+ {
307
+ "discriminator": "type",
308
+ "mapping": {
309
+ "RECTANGLE": {
310
+ "properties": {
311
+ "width": {
312
+ "type": "float32"
313
+ },
314
+ "height": {
315
+ "type": "float32"
316
+ }
317
+ },
318
+ "additionalProperties": true
319
+ },
320
+ "CIRCLE": {
321
+ "properties": {
322
+ "radius": {
323
+ "type": "float32"
324
+ }
325
+ },
326
+ "additionalProperties": true
327
+ }
328
+ }
329
+ }
330
+ ```
331
+
332
+ ### Recursive Types
333
+
334
+ You can define recursive schemas by using the `a.recursive` helper. This function accepts another function that outputs an object schema or a discriminator schema.
335
+
336
+ An important thing to note is that type inference doesn't work correctly for Recursive schemas. In order to satisfy Typescript you will need to define the type and then pass it to the function as a generic.
337
+
338
+ Additionally it is recommended to define an ID for any recursive schemas. If one is not specified arri will auto generate one.
339
+
340
+ ---
341
+
342
+ _If some TS wizard knows how to get type inference to work automatically for these recursive schemas, feel free to open a PR although I fear it will require a major refactor the existing type system._
343
+
344
+ **Usage**
345
+
346
+ ```ts
347
+ // the recursive type must be defined first
348
+ type BinaryTree = {
349
+ left: BinaryTree | null;
350
+ right: BinaryTree | null;
351
+ };
352
+
353
+ // pass the type to the helper
354
+ const BinaryTree = a.recursive<BinaryTree>(
355
+ (self) =>
356
+ // the resulting schema must be an object or discriminator
357
+ // it also must match the type you pass into the generic parameter
358
+ // or TS will yell at you
359
+ a.object({
360
+ left: a.nullable(self),
361
+ right: a.nullable(self),
362
+ }),
363
+ {
364
+ id: "BinaryTree",
365
+ },
366
+ );
367
+
368
+ a.validate(BinaryTree, {
369
+ left: {
370
+ left: null,
371
+ right: {
372
+ left: null,
373
+ right: null,
374
+ },
375
+ },
376
+ right: null,
377
+ }); // true
378
+ a.validate(BinaryTree, {
379
+ left: {
380
+ left: null,
381
+ right: {
382
+ left: true,
383
+ right: null,
384
+ },
385
+ },
386
+ right: null,
387
+ }); // false
388
+ ```
389
+
390
+ **Outputted JTD**
391
+
392
+ ```json
393
+ {
394
+ "properties": {
395
+ "left": {
396
+ "ref": "BinaryTree",
397
+ "nullable": true
398
+ },
399
+ "right": {
400
+ "ref": "BinaryTree",
401
+ "nullable": true
402
+ }
403
+ },
404
+ "additionalProperties": true,
405
+ "metadata": {
406
+ "id": "BinaryTree"
407
+ }
408
+ }
409
+ ```
410
+
411
+ ## Modifiers
412
+
413
+ ### Optional
414
+
415
+ Use `a.optional()` to make an object field optional.
416
+
417
+ ```ts
418
+ const User = a.object({
419
+ id: a.string(),
420
+ email: a.optional(a.string()),
421
+ date: a.timestamp();
422
+ })
423
+
424
+ /**
425
+ * Resulting type
426
+ * {
427
+ * id: string;
428
+ * email: string | undefined;
429
+ * date: Date;
430
+ * }
431
+ */
432
+ ```
433
+
434
+ **Outputted JTD**
435
+
436
+ ```json
437
+ {
438
+ "properties": {
439
+ "id": {
440
+ "type": "string"
441
+ },
442
+ "date": {
443
+ "type": "timestamp"
444
+ }
445
+ },
446
+ "optionalProperties": {
447
+ "email": {
448
+ "type": "string"
449
+ }
450
+ }
451
+ }
452
+ ```
453
+
454
+ ### Nullable
455
+
456
+ Use `a.nullable()` to make a particular type nullable
457
+
458
+ ```ts
459
+ const name = a.nullable(a.string());
460
+
461
+ /**
462
+ * Resulting type
463
+ * string | null
464
+ */
465
+ ```
466
+
467
+ **Outputted JTD**
468
+
469
+ ```json
470
+ {
471
+ "type": "string",
472
+ "nullable": true
473
+ }
474
+ ```
475
+
476
+ ### Clone
477
+
478
+ Copy another schema without copying it's metadata using the `a.clone()` helper
479
+
480
+ ```ts
481
+ const A = a.object(
482
+ {
483
+ a: a.string(),
484
+ b: a.float32(),
485
+ },
486
+ { id: "A" },
487
+ );
488
+ console.log(A.metadata.id); // "A"
489
+
490
+ const B = a.clone(A);
491
+ console.log(B.metadata.id); // undefined
492
+ ```
493
+
494
+ ### Extend
495
+
496
+ Extend an object schema with the `a.extend()` helper.
497
+
498
+ ```ts
499
+ const A = a.object({
500
+ a: a.string(),
501
+ b: a.float32(),
502
+ });
503
+ // { a: string; b: number; }
504
+
505
+ const B = a.object({
506
+ c: a.timestamp(),
507
+ });
508
+ // { c: Date }
509
+
510
+ const C = a.extend(A, B);
511
+ // { a: string; b: number; c: Date }
512
+ ```
513
+
514
+ ### Omit
515
+
516
+ Use `a.omit()` to create a new object schema with certain properties removed
517
+
518
+ ```ts
519
+ const A = a.object({
520
+ a: a.string(),
521
+ b: a.float32(),
522
+ });
523
+ // { a: string; b: number; }
524
+
525
+ const B = a.omit(A, ["a"]);
526
+ // { b: number; }
527
+ ```
528
+
529
+ ### Pick
530
+
531
+ Use `a.pick()` to create a new object schema with the a subset of properties from the parent object
532
+
533
+ ```ts
534
+ const A = a.object({
535
+ a: a.string(),
536
+ b: a.float32(),
537
+ c: a.timestamp(),
538
+ });
539
+ // { a: string; b: number; c: Date; }
540
+
541
+ const B = a.pick(A, ["a", "c"]);
542
+ // { a: string; c: Date; }
543
+ ```
544
+
545
+ ### Partial
546
+
547
+ Use `a.partial()` to create a new object schema that makes all of the properties of the parent schema optional.
548
+
549
+ ```ts
550
+ const A = a.object({
551
+ a: a.string(),
552
+ b: a.float32(),
553
+ c: a.timestamp(),
554
+ });
555
+ // { a: string; b: number; c: Date; }
556
+
557
+ const B = a.partial(A);
558
+ // { a: string | undefined; b: number | undefined; c: Date | undefined; }
559
+ ```
560
+
561
+ ## Utilities
562
+
563
+ ### Validate
564
+
565
+ Call `a.validate()` to validate an input against an arri schema. This method also acts as a type guard, so any `any` or `unknown` types that pass validation will automatically gain autocomplete for the validated fields
566
+
567
+ ```ts
568
+ const User = a.object({
569
+ id: a.string(),
570
+ name: a.string(),
571
+ });
572
+ a.validate(User, true); // false
573
+ a.validate(User, { id: "1", name: "john doe" }); // true
574
+
575
+ if (a.validate(User, someInput)) {
576
+ console.log(someInput.id); // intellisense works here
577
+ }
578
+ ```
579
+
580
+ ### Parse
581
+
582
+ Call `a.parse()` to parse a JSON string against an arri schema. It will also handle parsing normal objects as well.
583
+
584
+ ```ts
585
+ const User = a.object({
586
+ id: a.string(),
587
+ name: a.string(),
588
+ });
589
+
590
+ // returns a User if successful or throws a ValidationError if fails
591
+ const result = a.parse(User, jsonString);
592
+ ```
593
+
594
+ ### Safe Parse
595
+
596
+ A safer alternative to `a.parse()` that doesn't throw an error.
597
+
598
+ ```ts
599
+ const User = a.object({
600
+ id: a.string(),
601
+ name: a.string(),
602
+ });
603
+
604
+ const result = a.safeParse(User, jsonString);
605
+ if (result.success) {
606
+ console.log(result.value); // result.value will be User
607
+ } else {
608
+ console.error(result.error);
609
+ }
610
+ ```
611
+
612
+ ### Coerce
613
+
614
+ `a.coerce()` will attempt to convert inputs to the correct type. If it fails to convert the inputs it will throw a `ValidationError`
615
+
616
+ ```ts
617
+ const A = a.object({
618
+ a: a.string(),
619
+ b: a.boolean(),
620
+ c: a.float32(),
621
+ });
622
+
623
+ a.coerce(A, {
624
+ a: "1",
625
+ b: "true",
626
+ c: "500.24",
627
+ });
628
+ // { a: "1", b: true, c: 500.24 };
629
+ ```
630
+
631
+ ### Safe Coerce
632
+
633
+ `a.safeCoerce()` is an alternative to `a.coerce()` that doesn't throw.
634
+
635
+ ```ts
636
+ const A = a.object({
637
+ a: a.string(),
638
+ b: a.boolean(),
639
+ c: a.float32(),
640
+ });
641
+
642
+ const result = a.safeCoerce(A, someInput);
643
+
644
+ if (result.success) {
645
+ console.log(result.value);
646
+ } else {
647
+ console.error(result.error);
648
+ }
649
+ ```
650
+
651
+ ### Serialize
652
+
653
+ `a.serialize()` will take an input and serialize it to a valid JSON string.
654
+
655
+ ```ts
656
+ const User = a.object({
657
+ id: a.string(),
658
+ name: a.string(),
659
+ });
660
+
661
+ a.serialize(User, { id: "1", name: "john doe" });
662
+ // {"id":"1","name":"john doe"}
663
+ ```
664
+
665
+ Be aware that this function does not validate the input. So if you are passing in an any or unknown type into this function it is recommended that you validate it first.
666
+
667
+ ### Errors
668
+
669
+ Use `a.errors()` to get all of the validation errors of a given input.
670
+
671
+ ```ts
672
+ const User = a.object({
673
+ id: a.string(),
674
+ date: a.timestamp(),
675
+ });
676
+
677
+ a.errors(User, { id: 1, date: "hello world" });
678
+ /**
679
+ * [
680
+ * {
681
+ * instancePath: "/id",
682
+ * schemaPath: "/properties/id/type",
683
+ * message: "Expected string",
684
+ * },
685
+ * {
686
+ * instancePath: "/date",
687
+ * schemaPath: "/properties/id/type",
688
+ * message: "Expected instanceof Date",
689
+ * }
690
+ * ]
691
+ *
692
+ */
693
+ ```
694
+
695
+ ## Compiled Validators
696
+
697
+ `@arrirpc/schema` comes with a high performance JIT compiler that transforms Arri Schemas into highly optimized validation, parsing, serialization functions.
698
+
699
+ ```ts
700
+ const User = a.object({
701
+ id: a.string(),
702
+ email: a.nullable(a.string()),
703
+ created: a.timestamp(),
704
+ });
705
+
706
+ const $$User = a.compile(User);
707
+
708
+ $$User.validate(someInput);
709
+ $$User.parse(someJson);
710
+ $$User.serialize({ id: "1", email: null, created: new Date() });
711
+ ```
712
+
713
+ In most cases, the compiled validators will be much faster than the standard utilities. However there is some overhead with compiling the schemas so ideally each validator would be compiled once. Additionally the resulting methods make use of eval so they can only be used in an environment that you control such as a backend server. They WILL NOT work in a browser environment.
714
+
715
+ You can also use `a.compile` for code generation. The compiler result gives you access to the generated function bodies.
716
+
717
+ ```ts
718
+ $$User.compiledCode.validate; // the generated validation code
719
+ $$User.compiledCode.parse; // the generated parsing code
720
+ $$User.compiledCode.serialize; // the generated serialization code
721
+ ```
722
+
723
+ ## Metadata
724
+
725
+ Metadata is used during cross-language code generation. Arri schemas allow you to specify the following metadata fields:
726
+
727
+ - id - Will be used as the type name in any arri client generators
728
+ - description - Will be added as a description comment above any generated types
729
+ - isDeprecated - Will mark any generated code with the deprecation annotation of target language
730
+
731
+ ### Examples
732
+
733
+ A schema with this metadata:
734
+
735
+ ```ts
736
+ // metadata object
737
+ const BookSchema = a.object(
738
+ {
739
+ title: a.string(),
740
+ author: a.string(),
741
+ publishDate: a.timestamp(),
742
+ },
743
+ {
744
+ id: "Book",
745
+ description: "This is a book",
746
+ },
747
+ );
748
+ ```
749
+
750
+ will produce types that look something like this during codegen.
751
+
752
+ **Typescript**
753
+
754
+ ```ts
755
+ /**
756
+ * This is a book
757
+ */
758
+ interface Book {
759
+ title: string;
760
+ author: string;
761
+ publishDate: Date;
762
+ }
763
+ ```
764
+
765
+ **Rust**
766
+
767
+ ```rust
768
+ /// This is a book
769
+ struct Book {
770
+ title: String,
771
+ author: String,
772
+ publish_date: DateTime<FixedOffset>
773
+ }
774
+ ```
775
+
776
+ **Dart**
777
+
778
+ ```dart
779
+ /// This is a book
780
+ class Book {
781
+ final String title;
782
+ final String author;
783
+ final DateTime publishDate;
784
+ const Book({
785
+ required this.title,
786
+ required this.author,
787
+ required this.publishDate,
788
+ });
789
+ }
790
+ ```
791
+
792
+ **Kotlin**
793
+
794
+ ```kotlin
795
+ /**
796
+ * This is a book
797
+ */
798
+ data class Book(
799
+ val title: String,
800
+ val author: String,
801
+ val publishDate: Instant,
802
+ )
803
+ ```
804
+
805
+ ### ID Shorthand (Experimental)
806
+
807
+ Because IDs are really important for producing concise type names. Arri validate also provides an _experimental\*_ shorthand for defining IDs of objects, discriminators, and recursive types.
808
+
809
+ ```ts
810
+ // ID will be set to "Book"
811
+ const BookSchema = a.object("Book", {
812
+ title: a.string(),
813
+ author: a.string(),
814
+ publishDate: a.timestamp(),
815
+ });
816
+
817
+ // ID will be set to "Message"
818
+ const MessageSchema = a.discriminator("Message", "type", {
819
+ TEXT: a.object({
820
+ userId: a.string(),
821
+ content: a.string(),
822
+ }),
823
+ IMAGE: a.object({
824
+ userId: a.string(),
825
+ imageUrl: a.string(),
826
+ }),
827
+ });
828
+
829
+ // ID will be set to "BTree"
830
+ const BinaryTreeSchema = a.recursive("BTree", (self) =>
831
+ a.object({
832
+ left: a.nullable(self),
833
+ right: a.nullable(self),
834
+ }),
835
+ );
836
+ ```
837
+
838
+ \* Because this is experimental it may be removed in the future. The main thing I'm testings is whether added convenience is worth the overhead of maintaining 2 versions of each of these functions. The shorthand could also introduce unintended confusion for users of this library as it creates two places to look for an id.
839
+
840
+ ## Benchmarks
841
+
842
+ _Last Updated: 2024-03-19_
843
+
844
+ All benchmarks were run on my personal desktop. You can view the methodology used in [./benchmarks/src](./benchmark/src).
845
+
846
+ ```txt
847
+ OS - Pop!_OS 22.04 LTS
848
+ CPU - AMD Ryzen 9 5900 12-Core Processor
849
+ RAM - 32GB
850
+ Graphics - AMD® Radeon rx 6900 xt
851
+ ```
852
+
853
+ ### Objects
854
+
855
+ The following data was used in these benchmarks. Relevant schemas were created in each of the mentioned libraries.
856
+
857
+ ```ts
858
+ {
859
+ id: 12345,
860
+ role: "moderator",
861
+ name: "John Doe",
862
+ email: null,
863
+ createdAt: 0,
864
+ updatedAt: 0,
865
+ settings: {
866
+ preferredTheme: "system",
867
+ allowNotifications: true,
868
+ },
869
+ recentNotifications: [
870
+ {
871
+ type: "POST_LIKE",
872
+ postId: "1",
873
+ userId: "2",
874
+ },
875
+ {
876
+ type: "POST_COMMENT",
877
+ postId: "1",
878
+ userId: "1",
879
+ commentText: "",
880
+ },
881
+ ],
882
+ };
883
+ ```
884
+
885
+ #### Validation
886
+
887
+ | Library | op/s |
888
+ | ---------------------------- | ----------- |
889
+ | **Arri (Compiled)** | 122,753,978 |
890
+ | Typebox (Compiled) | 63,131,651 |
891
+ | Ajv -JTD (Compiled) | 37,814,896 |
892
+ | Ajv - JTD | 29,402,413 |
893
+ | Ajv - JSON Schema (Compiled) | 12,003,432 |
894
+ | Ajv -JSON Schema | 10,057,501 |
895
+ | **Arri** | 2,131,823 |
896
+ | Typebox | 93,5599 |
897
+ | Zod | 52,1357 |
898
+
899
+ #### Parsing
900
+
901
+ | Library | op/s |
902
+ | ------------------- | ------- |
903
+ | JSON.parse | 749,534 |
904
+ | **Arri (Compiled)** | 728,382 |
905
+ | **Arri** | 378,175 |
906
+ | Ajv -JTD (Compiled) | 241,107 |
907
+
908
+ #### Serialization
909
+
910
+ | Library | op/s |
911
+ | ------------------------------------------ | --------- |
912
+ | **Arri (Compiled)** | 4,272,430 |
913
+ | **Arri (Compiled) Validate and Serialize** | 3,846,453 |
914
+ | Ajv - JTD (Compiled) | 2,012,894 |
915
+ | JSON.stringify | 938,289 |
916
+ | Arri | 481,985 |
917
+
918
+ #### Coercion
919
+
920
+ | Library | op/s |
921
+ | -------- | ------- |
922
+ | **Arri** | 818,963 |
923
+ | Zod | 466,092 |
924
+ | Typebox | 209,363 |
925
+
926
+ ### Integers
927
+
928
+ The following benchmarks measure how quickly each library operates on a single integer value.
929
+
930
+ #### Validation
931
+
932
+ | Library | op/s |
933
+ | ---------------------------- | ----------- |
934
+ | Ajv - JSON Schema (Compiled) | 329,332,736 |
935
+ | **Arri (Compiled)** | 201,644,167 |
936
+ | **Arri** | 186,634,732 |
937
+ | Ajv - JTD (Compiled) | 151,044,902 |
938
+ | Typebox (Compiled) | 110,692,029 |
939
+ | Ajv - JTD | 48,200,004 |
940
+ | Ajv - JSON Schema | 47,840,571 |
941
+ | Typebox | 42,363,980 |
942
+ | Zod | 1,266,268 |
943
+
944
+ #### Parsing
945
+
946
+ | Library | op/s |
947
+ | -------------------- | ----------- |
948
+ | **Arri (Compiled)** | 138,189,123 |
949
+ | **Arri** | 136,995,619 |
950
+ | JSON.parse() | 19,911,721 |
951
+ | Ajv - JTD (Compiled) | 8,996,081 |
952
+
953
+ #### Serialization
954
+
955
+ | Library | op/s |
956
+ | -------------------- | ----------- |
957
+ | Ajv - JTD (Compiled) | 198,980,679 |
958
+ | **Arri (Compiled)** | 190,386,426 |
959
+ | **Arri** | 114,799,692 |
960
+ | JSON.stringify | 21,433,854 |
961
+
962
+ #### Coercion
963
+
964
+ | Library | op/s |
965
+ | ----------------- | ----------- |
966
+ | Arri | 117,854,219 |
967
+ | TypeBox | 34,633,126 |
968
+ | Ajv - JSON Schema | 28,016,735 |
969
+ | Zod | 1,586,546 |
970
+
971
+ ## Development
972
+
973
+ ### Building
974
+
975
+ Run `nx build @arrirpc/schema` to build the library.
976
+
977
+ ### Running unit tests
978
+
979
+ Run `nx test @arrirpc/schema` to execute the unit tests via Vitest
980
+
981
+ ### Benchmarking
982
+
983
+ Run `nx benchmark @arrirpc/schema` to execute benchmarks