1import { assert } from "@effect/vitest"
2import type { Options as AjvOptions } from "ajv"
3import { Effect, JsonSchema, Option, Predicate, Schema, SchemaGetter } from "effect"
4import { describe, it } from "vitest"
5import { assertTrue, deepStrictEqual, throws } from "../utils/assert.ts"
6
7// oxlint-disable-next-line @typescript-eslint/no-require-imports
8const Ajv2020 = require("ajv/dist/2020")
9
10const baseAjvOptions: AjvOptions = {
11 allErrors: true,
12 strict: false, // warns/throws on unknown keywords depending on Ajv version
13 validateSchema: true,
14 code: { esm: true } // optional
15}
16
17const ajvDraft2020_12 = new Ajv2020.default(baseAjvOptions)
18
19function assertUnsupportedSchema(
20 schema: Schema.Constraint,
21 message: string,
22 options?: Schema.ToJsonSchemaOptions
23) {
24 throws(() => Schema.toJsonSchemaDocument(schema, options), message)
25}
26
27function assertJsonSchemaDocument<T, E, RD>(
28 schema: Schema.Codec<T, E, RD, never>,
29 expected: { schema: JsonSchema.JsonSchema; definitions?: JsonSchema.Definitions },
30 options?: Schema.ToJsonSchemaOptions
31) {
32 const document = Schema.toJsonSchemaDocument(schema, {
33 onExcessProperty: "error",
34 ...options
35 })
36 deepStrictEqual(document, {
37 dialect: "draft-2020-12",
38 schema: expected.schema,
39 definitions: expected.definitions ?? {}
40 })
41 const jsonSchema = {
42 $schema: JsonSchema.META_SCHEMA_URI_DRAFT_2020_12,
43 ...document.schema,
44 $defs: document.definitions
45 }
46 const valid = ajvDraft2020_12.validateSchema(jsonSchema)
47 assertTrue(valid)
48}
49
50function assertJsonSchemaAcceptsEffectValues<T, E, RD>(
51 schema: Schema.Codec<T, E, RD, never>,
52 values: ReadonlyArray<unknown>
53) {
54 const document = Schema.toJsonSchemaDocument(schema, { onExcessProperty: "error" })
55 const validate = ajvDraft2020_12.compile({
56 $schema: JsonSchema.META_SCHEMA_URI_DRAFT_2020_12,
57 ...document.schema,
58 $defs: document.definitions
59 })
60 const is = Schema.is(schema)
61 for (const value of values) {
62 if (is(value)) {
63 assertTrue(validate(value), `JSON Schema rejected ${JSON.stringify(value)}`)
64 }
65 }
66}
67
68describe("toJsonSchemaDocument", () => {
69 describe("unsupported schemas", () => {
70 it("rejects tuple post-rest elements", () => {
71 assertUnsupportedSchema(
72 Schema.TupleWithRest(Schema.Tuple([]), [Schema.Finite, Schema.String]),
73 `Invalid schema representation document\n at ["representation"]["rest"]`
74 )
75 })
76
77 it("rejects symbol property names", () => {
78 const a = Symbol.for("effect/Schema/test/a")
79 assertUnsupportedSchema(
80 Schema.Struct({ [a]: Schema.String }),
81 "Objects property names must be strings"
82 )
83 })
84 })
85
86 it("Record(Symbol, Finite)", () => {
87 assertJsonSchemaDocument(Schema.Record(Schema.Symbol, Schema.Finite), {
88 schema: {
89 type: "object",
90 patternProperties: {
91 "^Symbol\\(([\\s\\S]*)\\)$": { type: "number" }
92 },
93 additionalProperties: false
94 }
95 })
96 })
97
98 it("emits content annotations", () => {
99 assertJsonSchemaDocument(
100 Schema.String.annotate({
101 description: "encoded payload",
102 contentMediaType: "application/json",
103 contentSchema: { type: "number" }
104 }),
105 {
106 schema: {
107 type: "string",
108 description: "encoded payload",
109 contentMediaType: "application/json",
110 contentSchema: { type: "number" }
111 }
112 }
113 )
114 })
115
116 describe("options", () => {
117 it("generateDescriptions: true", () => {
118 assertJsonSchemaDocument(
119 Schema.String.annotate({ expected: "b" }),
120 {
121 schema: {
122 "type": "string",
123 "description": "b"
124 }
125 },
126 { generateDescriptions: true }
127 )
128 assertJsonSchemaDocument(
129 Schema.String.annotate({ description: "a", expected: "b" }),
130 {
131 schema: {
132 "type": "string",
133 "description": "a"
134 }
135 },
136 { generateDescriptions: true }
137 )
138 })
139
140 describe("onExcessProperty", () => {
141 it("defaults to ignore", () => {
142 deepStrictEqual(Schema.toJsonSchemaDocument(Schema.Struct({})).schema, {
143 not: { type: "null" }
144 })
145 deepStrictEqual(Schema.toJsonSchemaDocument(Schema.Struct({ a: Schema.String })).schema, {
146 type: "object",
147 properties: { a: { type: "string" } },
148 required: ["a"],
149 additionalProperties: true
150 })
151 })
152
153 it(`ignore (default)`, () => {
154 const schema = Schema.Struct({ a: Schema.String })
155
156 assertJsonSchemaDocument(schema, {
157 schema: {
158 "type": "object",
159 "properties": {
160 "a": {
161 "type": "string"
162 }
163 },
164 "required": ["a"],
165 "additionalProperties": true
166 }
167 }, {
168 onExcessProperty: "ignore"
169 })
170 })
171
172 it(`error`, () => {
173 const schema = Schema.Struct({ a: Schema.String })
174
175 assertJsonSchemaDocument(schema, {
176 schema: {
177 "type": "object",
178 "properties": {
179 "a": {
180 "type": "string"
181 }
182 },
183 "required": ["a"],
184 "additionalProperties": false
185 }
186 }, {
187 onExcessProperty: "error"
188 })
189 })
190
191 it("applies to properties not covered by patternProperties", () => {
192 assertJsonSchemaDocument(Schema.Record(Schema.TemplateLiteral(["x-", Schema.String]), Schema.Number), {
193 schema: {
194 type: "object",
195 patternProperties: {
196 "^x-[\\s\\S]*?$": {
197 anyOf: [
198 { type: "number" },
199 { type: "string", enum: ["Infinity", "-Infinity", "NaN"] }
200 ]
201 }
202 },
203 additionalProperties: false
204 }
205 }, {
206 onExcessProperty: "error"
207 })
208 })
209
210 it("uses the representable part of an index-signature key", () => {
211 const key = Schema.String.check(Schema.makeFilter((value) => value.startsWith("x-")))
212
213 assertJsonSchemaDocument(Schema.Record(key, Schema.String), {
214 schema: {
215 type: "object",
216 propertyNames: { type: "string" },
217 additionalProperties: { type: "string" }
218 }
219 }, {
220 onExcessProperty: "error"
221 })
222 })
223 })
224
225 describe("includeAnnotationKey", () => {
226 it("passthroughs matching annotation keys at schema level", () => {
227 assertJsonSchemaDocument(
228 Schema.String.annotate({
229 title: "Name",
230 description: "A name",
231 "markdownDescription": "The **name** field"
232 }),
233 {
234 schema: {
235 "type": "string",
236 "title": "Name",
237 "description": "A name",
238 "markdownDescription": "The **name** field"
239 }
240 },
241 { includeAnnotationKey: (key) => key === "markdownDescription" }
242 )
243 })
244
245 it("does not include keys not matching the predicate", () => {
246 assertJsonSchemaDocument(
247 Schema.String.annotate({
248 title: "Name",
249 description: "A name",
250 "markdownDescription": "The **name** field",
251 "customKey": "value"
252 }),
253 {
254 schema: {
255 "type": "string",
256 "title": "Name",
257 "description": "A name",
258 "markdownDescription": "The **name** field"
259 }
260 },
261 { includeAnnotationKey: (key) => key === "markdownDescription" }
262 )
263 })
264
265 it("does not include matching keys with undefined values", () => {
266 assertJsonSchemaDocument(
267 Schema.String.annotate({
268 "x-missing": undefined,
269 "x-value": "value"
270 }),
271 {
272 schema: {
273 "type": "string",
274 "x-value": "value"
275 }
276 },
277 { includeAnnotationKey: (key) => key.startsWith("x-") }
278 )
279 })
280
281 it("standard keys are always included regardless of predicate", () => {
282 assertJsonSchemaDocument(
283 Schema.String.annotate({
284 title: "Name",
285 description: "A name",
286 default: "hello"
287 }),
288 {
289 schema: {
290 "type": "string",
291 "title": "Name",
292 "description": "A name",
293 "default": "hello"
294 }
295 },
296 { includeAnnotationKey: (_key) => false }
297 )
298 })
299
300 it("passthroughs at property level in structs", () => {
301 const schema = Schema.Struct({
302 name: Schema.String.annotate({
303 description: "A name",
304 "markdownDescription": "The **name** field"
305 }),
306 tag: Schema.String.annotate({
307 description: "A tag",
308 "defaultSnippets": [{ label: "v1", body: "v1" }]
309 })
310 })
311 assertJsonSchemaDocument(
312 schema,
313 {
314 schema: {
315 "type": "object",
316 "properties": {
317 "name": {
318 "type": "string",
319 "description": "A name",
320 "markdownDescription": "The **name** field"
321 },
322 "tag": {
323 "type": "string",
324 "description": "A tag",
325 "defaultSnippets": [{ label: "v1", body: "v1" }]
326 }
327 },
328 "required": ["name", "tag"],
329 "additionalProperties": false
330 }
331 },
332 { includeAnnotationKey: (key) => key === "markdownDescription" || key === "defaultSnippets" }
333 )
334 })
335
336 it("passthroughs at check level", () => {
337 const schema = Schema.String
338 .annotate({ description: "A string" })
339 .pipe(
340 Schema.check(
341 Schema.isMinLength(1, {
342 "x-check-annotation": true
343 })
344 )
345 )
346 assertJsonSchemaDocument(
347 schema,
348 {
349 schema: {
350 "type": "string",
351 "description": "A string",
352 "allOf": [{
353 "minLength": 1,
354 "x-check-annotation": true
355 }]
356 }
357 },
358 { includeAnnotationKey: (key) => key.startsWith("x-") }
359 )
360 })
361
362 it("passthroughs x- prefixed vendor extensions", () => {
363 assertJsonSchemaDocument(
364 Schema.String.annotate({
365 description: "A value",
366 "x-custom": true,
367 "x-extension": { foo: "bar" }
368 }),
369 {
370 schema: {
371 "type": "string",
372 "description": "A value",
373 "x-custom": true,
374 "x-extension": { foo: "bar" }
375 }
376 },
377 { includeAnnotationKey: (key) => key.startsWith("x-") }
378 )
379 })
380 })
381 })
382
383 it("emits standard annotations", () => {
384 const schema = Schema.String.annotate({
385 title: "a",
386 description: "b",
387 default: "c",
388 examples: ["d"],
389 readOnly: true,
390 writeOnly: true
391 })
392 assertJsonSchemaDocument(schema, {
393 schema: {
394 "type": "string",
395 "title": "a",
396 "description": "b",
397 "default": "c",
398 "examples": ["d"],
399 "readOnly": true,
400 "writeOnly": true
401 }
402 })
403 })
404
405 describe("identifiers", () => {
406 it(`escapes "~" and "/" in JSON Pointer references`, () => {
407 const S = Schema.String.annotate({ identifier: "id~a/b" })
408 assertJsonSchemaDocument(
409 S,
410 {
411 schema: { "$ref": "#/$defs/id~0a~1b" },
412 definitions: {
413 "id~a/b": { "type": "string" }
414 }
415 }
416 )
417 })
418
419 it("reuses a definition for repeated occurrences of the same identified AST", () => {
420 const S = Schema.String.annotate({ identifier: "id" })
421 assertJsonSchemaDocument(
422 Schema.Union([S, S]),
423 {
424 schema: {
425 "anyOf": [
426 { "$ref": "#/$defs/id" },
427 { "$ref": "#/$defs/id" }
428 ]
429 },
430 definitions: {
431 id: { "type": "string" }
432 }
433 }
434 )
435 })
436
437 it("suffixes duplicate identifiers on different schemas", () => {
438 const S = Schema.Union([
439 Schema.String.annotate({ identifier: "id", description: "a" }),
440 Schema.String.annotate({ identifier: "id", description: "b" })
441 ])
442 assertJsonSchemaDocument(S, {
443 schema: {
444 anyOf: [
445 { $ref: "#/$defs/id" },
446 { $ref: "#/$defs/id_1" }
447 ]
448 },
449 definitions: {
450 id: { type: "string", description: "a" },
451 id_1: { type: "string", description: "b" }
452 }
453 })
454 })
455
456 it("reuses one definition when the same identified AST appears in different schema shapes", () => {
457 const X = Schema.String.annotate({ title: "X", identifier: "X" })
458 const S = Schema.Struct({
459 a: X,
460 b: Schema.NullOr(X),
461 c: Schema.optionalKey(X),
462 d: Schema.optionalKey(Schema.NullOr(X)),
463 e: Schema.NullOr(X).pipe(
464 Schema.encodeTo(Schema.optionalKey(X), {
465 decode: SchemaGetter.transformOptional(Option.orElseSome(() => null)),
466 encode: SchemaGetter.transformOptional(Option.filter(Predicate.isNotNull))
467 })
468 )
469 })
470 assertJsonSchemaDocument(S, {
471 schema: {
472 "type": "object",
473 "properties": {
474 "a": {
475 "$ref": "#/$defs/X"
476 },
477 "b": {
478 "anyOf": [
479 {
480 "$ref": "#/$defs/X"
481 },
482 {
483 "type": "null"
484 }
485 ]
486 },
487 "c": {
488 "$ref": "#/$defs/X"
489 },
490 "d": {
491 "anyOf": [
492 {
493 "$ref": "#/$defs/X"
494 },
495 {
496 "type": "null"
497 }
498 ]
499 },
500 "e": {
501 "$ref": "#/$defs/X"
502 }
503 },
504 "required": [
505 "a",
506 "b"
507 ],
508 "additionalProperties": false
509 },
510 definitions: {
511 "X": {
512 "type": "string",
513 "title": "X"
514 }
515 }
516 })
517 })
518 })
519
520 describe("Declaration", () => {
521 it("opaque Declaration", () => {
522 assertJsonSchemaDocument(Schema.instanceOf(URL), {
523 schema: {}
524 })
525 })
526
527 it("Date", () => {
528 const schema = Schema.Date
529 assertJsonSchemaDocument(schema, {
530 schema: {
531 "type": "string"
532 }
533 })
534 })
535
536 it("URL", () => {
537 const schema = Schema.URL
538 assertJsonSchemaDocument(schema, {
539 schema: {
540 "type": "string"
541 }
542 })
543 })
544
545 it("Error", () => {
546 const schema = Schema.ErrorInstance()
547 assertJsonSchemaDocument(schema, {
548 schema: {
549 "type": "object",
550 "properties": {
551 "name": { "type": "string" },
552 "message": { "type": "string" },
553 "stack": { "type": "string" },
554 "cause": {}
555 },
556 "required": ["message"],
557 "additionalProperties": false
558 }
559 })
560 })
561
562 it("RegExp", () => {
563 const schema = Schema.RegExp
564 assertJsonSchemaDocument(schema, {
565 schema: {
566 "type": "object",
567 "properties": {
568 "source": { "type": "string" },
569 "flags": { "type": "string" }
570 },
571 "required": ["source", "flags"],
572 "additionalProperties": false
573 }
574 })
575 })
576
577 it("Uint8Array", () => {
578 const schema = Schema.Uint8Array
579 assertJsonSchemaDocument(schema, {
580 schema: {
581 "type": "string",
582 "format": "byte",
583 "contentEncoding": "base64"
584 }
585 })
586 })
587
588 it("Duration", () => {
589 const schema = Schema.Duration
590 assertJsonSchemaDocument(schema, {
591 schema: {
592 "anyOf": [
593 {
594 "type": "object",
595 "properties": {
596 "_tag": {
597 "type": "string",
598 "enum": ["Infinity"]
599 }
600 },
601 "required": ["_tag"],
602 "additionalProperties": false
603 },
604 {
605 "type": "object",
606 "properties": {
607 "_tag": {
608 "type": "string",
609 "enum": ["NegativeInfinity"]
610 }
611 },
612 "required": ["_tag"],
613 "additionalProperties": false
614 },
615 {
616 "type": "object",
617 "properties": {
618 "_tag": {
619 "type": "string",
620 "enum": ["Nanos"]
621 },
622 "value": {
623 "type": "string",
624 "pattern": "^-?\\d+$"
625 }
626 },
627 "required": ["_tag", "value"],
628 "additionalProperties": false
629 },
630 {
631 "type": "object",
632 "properties": {
633 "_tag": {
634 "type": "string",
635 "enum": ["Millis"]
636 },
637 "value": {
638 "type": "integer"
639 }
640 },
641 "required": ["_tag", "value"],
642 "additionalProperties": false
643 }
644 ]
645 }
646 })
647 })
648
649 it("Option(String)", () => {
650 const schema = Schema.Option(Schema.String)
651 assertJsonSchemaDocument(schema, {
652 schema: {
653 "anyOf": [
654 {
655 "type": "object",
656 "properties": {
657 "_tag": {
658 "type": "string",
659 "enum": ["Some"]
660 },
661 "value": {
662 "type": "string"
663 }
664 },
665 "required": ["_tag", "value"],
666 "additionalProperties": false
667 },
668 {
669 "type": "object",
670 "properties": {
671 "_tag": {
672 "type": "string",
673 "enum": ["None"]
674 }
675 },
676 "required": ["_tag"],
677 "additionalProperties": false
678 }
679 ]
680 }
681 })
682 })
683 })
684
685 it("Any", () => {
686 const schema = Schema.Any
687 assertJsonSchemaDocument(
688 schema,
689 {
690 schema: {}
691 }
692 )
693 assertJsonSchemaDocument(
694 schema.annotate({ description: "a" }),
695 {
696 schema: {
697 "description": "a"
698 }
699 }
700 )
701 })
702
703 it("Json", () => {
704 const schema = Schema.Json
705 assertJsonSchemaDocument(
706 schema,
707 {
708 schema: {}
709 }
710 )
711 })
712
713 it("MutableJson", () => {
714 const schema = Schema.MutableJson
715 assertJsonSchemaDocument(
716 schema,
717 {
718 schema: {}
719 }
720 )
721 })
722
723 it("Unknown", () => {
724 const schema = Schema.Unknown
725 assertJsonSchemaDocument(
726 schema,
727 {
728 schema: {}
729 }
730 )
731 assertJsonSchemaDocument(
732 schema.annotate({ description: "a" }),
733 {
734 schema: {}
735 }
736 )
737 })
738
739 it("Void", () => {
740 const schema = Schema.Void
741 assertJsonSchemaDocument(
742 schema,
743 {
744 schema: {
745 "type": "null"
746 }
747 }
748 )
749 assertJsonSchemaDocument(
750 schema.annotate({ description: "a" }),
751 {
752 schema: {
753 "type": "null"
754 }
755 }
756 )
757 })
758
759 it("Undefined", () => {
760 const schema = Schema.Undefined
761 assertJsonSchemaDocument(
762 schema,
763 {
764 schema: {
765 "type": "null"
766 }
767 }
768 )
769 assertJsonSchemaDocument(
770 schema.annotate({ description: "a" }),
771 {
772 schema: {
773 "type": "null"
774 }
775 }
776 )
777 })
778
779 it("BigInt", () => {
780 const schema = Schema.BigInt
781 assertJsonSchemaDocument(
782 schema,
783 {
784 schema: {
785 "type": "string",
786 "pattern": "^-?\\d+$"
787 }
788 }
789 )
790 })
791
792 it("Symbol", () => {
793 const schema = Schema.Symbol
794 assertJsonSchemaDocument(
795 schema,
796 {
797 schema: {
798 "type": "string",
799 "pattern": "^Symbol\\(([\\s\\S]*)\\)$"
800 }
801 }
802 )
803 })
804
805 it("UniqueSymbol", () => {
806 const schema = Schema.UniqueSymbol(Symbol.for("a"))
807 assertJsonSchemaDocument(
808 schema,
809 {
810 schema: {
811 "type": "string",
812 "enum": ["Symbol(a)"]
813 }
814 }
815 )
816
817 assertJsonSchemaDocument(
818 Schema.UniqueSymbol(Symbol("a")),
819 {
820 schema: {
821 "not": {}
822 }
823 }
824 )
825 })
826
827 it("Never", () => {
828 const schema = Schema.Never
829 assertJsonSchemaDocument(
830 schema,
831 {
832 schema: {
833 "not": {}
834 }
835 }
836 )
837 assertJsonSchemaDocument(
838 schema.annotate({ description: "a" }),
839 {
840 schema: {
841 "description": "a",
842 "not": {}
843 }
844 }
845 )
846 })
847
848 it("Null", () => {
849 const schema = Schema.Null
850 assertJsonSchemaDocument(
851 schema,
852 {
853 schema: {
854 "type": "null"
855 }
856 }
857 )
858 assertJsonSchemaDocument(
859 schema.annotate({ description: "a" }),
860 {
861 schema: {
862 "type": "null",
863 "description": "a"
864 }
865 }
866 )
867 })
868
869 describe("String", () => {
870 it("String", () => {
871 assertJsonSchemaDocument(
872 Schema.String,
873 {
874 schema: {
875 "type": "string"
876 }
877 }
878 )
879 })
880
881 it("String & annotate", () => {
882 assertJsonSchemaDocument(
883 Schema.String.annotate({ description: "a" }),
884 {
885 schema: {
886 "type": "string",
887 "description": "a"
888 }
889 }
890 )
891 })
892
893 it("ignores annotateKey annotations when the schema is not contextual", () => {
894 assertJsonSchemaDocument(
895 Schema.String.annotateKey({
896 description: "a"
897 }),
898 {
899 schema: {
900 "type": "string"
901 }
902 }
903 )
904 })
905
906 it("String & check", () => {
907 assertJsonSchemaDocument(
908 Schema.String.check(Schema.isMinLength(2)),
909 {
910 schema: {
911 "type": "string",
912 "minLength": 1
913 }
914 }
915 )
916 })
917
918 it("String & custom check without annotation", () => {
919 assertJsonSchemaDocument(
920 Schema.String.check(Schema.makeFilter(() => true)),
921 {
922 schema: {
923 "type": "string"
924 }
925 }
926 )
927 })
928
929 it("String & annotate & check", () => {
930 assertJsonSchemaDocument(
931 Schema.String.annotate({ description: "a" }).check(Schema.isMinLength(2)),
932 {
933 schema: {
934 "type": "string",
935 "description": "a",
936 "minLength": 1
937 }
938 }
939 )
940 })
941
942 it("String & check & annotate", () => {
943 assertJsonSchemaDocument(
944 Schema.String.check(Schema.isMinLength(2)).annotate({
945 description: "a"
946 }),
947 {
948 schema: {
949 "type": "string",
950 "minLength": 1,
951 "description": "a"
952 }
953 }
954 )
955 })
956
957 it("String & check & check", () => {
958 assertJsonSchemaDocument(
959 Schema.String.check(Schema.isMinLength(2), Schema.isMaxLength(3)),
960 {
961 schema: {
962 "type": "string",
963 "minLength": 1,
964 "maxLength": 3
965 }
966 }
967 )
968 })
969
970 it("String & annotate & check & check", () => {
971 assertJsonSchemaDocument(
972 Schema.String.annotate({ description: "a" }).check(Schema.isMinLength(2), Schema.isMaxLength(3)),
973 {
974 schema: {
975 "type": "string",
976 "description": "a",
977 "minLength": 1,
978 "maxLength": 3
979 }
980 }
981 )
982 })
983
984 it("String & check & check & annotate", () => {
985 assertJsonSchemaDocument(
986 Schema.String.check(Schema.isMinLength(2), Schema.isMaxLength(3)).annotate({
987 description: "a"
988 }),
989 {
990 schema: {
991 "type": "string",
992 "minLength": 1,
993 "maxLength": 3,
994 "description": "a"
995 }
996 }
997 )
998 })
999
1000 it("String & annotate & check & check & annotate", () => {
1001 assertJsonSchemaDocument(
1002 Schema.String.annotate({ description: "a" }).check(
1003 Schema.isMinLength(2),
1004 Schema.isMaxLength(3, { description: "c" })
1005 ),
1006 {
1007 schema: {
1008 "type": "string",
1009 "description": "a",
1010 "minLength": 1,
1011 "allOf": [
1012 {
1013 "maxLength": 3,
1014 "description": "c"
1015 }
1016 ]
1017 }
1018 }
1019 )
1020 })
1021
1022 it("String & check & annotations & check & annotations", () => {
1023 assertJsonSchemaDocument(
1024 Schema.String.check(
1025 Schema.isMinLength(2, { description: "b" }),
1026 Schema.isMaxLength(3, { description: "c" })
1027 ),
1028 {
1029 schema: {
1030 "type": "string",
1031 "minLength": 1,
1032 "description": "b",
1033 "allOf": [
1034 {
1035 "maxLength": 3,
1036 "description": "c"
1037 }
1038 ]
1039 }
1040 }
1041 )
1042 })
1043
1044 it("String & annotations & check & annotations & check & annotations", () => {
1045 assertJsonSchemaDocument(
1046 Schema.String.annotate({ description: "a" }).check(
1047 Schema.isMinLength(2, { description: "b" }),
1048 Schema.isMaxLength(3, { description: "c" })
1049 ),
1050 {
1051 schema: {
1052 "type": "string",
1053 "description": "a",
1054 "allOf": [
1055 {
1056 "minLength": 1,
1057 "description": "b"
1058 },
1059 {
1060 "maxLength": 3,
1061 "description": "c"
1062 }
1063 ]
1064 }
1065 }
1066 )
1067 })
1068
1069 describe("checks", () => {
1070 it("isPattern", () => {
1071 assertJsonSchemaDocument(Schema.String.check(Schema.isPattern(/^abb+$/u)), {
1072 schema: {
1073 "type": "string",
1074 "pattern": "^abb+$"
1075 }
1076 })
1077 })
1078
1079 it("omits isPattern when its flags cannot be represented", () => {
1080 for (const regExp of [/^abb+$/, /^abb+$/y, /^abb+$/iu, /^abb+$/mu, /^abb+$/su]) {
1081 assertJsonSchemaDocument(Schema.String.check(Schema.isPattern(regExp)), {
1082 schema: { type: "string" }
1083 })
1084 }
1085 })
1086
1087 it("preserves representable isPattern semantics", () => {
1088 for (
1089 const [regExp, pattern] of [
1090 [/^abb+$/gu, "^abb+$"],
1091 [/abb+/uy, "^(?:abb+)"],
1092 [/abb+/guy, "^(?:abb+)"],
1093 [/^[é]+$/u, "^[é]+$"],
1094 [/^[a-z]+$/u, "^[a-z]+$"],
1095 [/^[\x20-\uD7FF\uE000-\uFFFF]+$/u, "^[\\x20-\\uD7FF\\uE000-\\uFFFF]+$"],
1096 [/^[a-b-\uE000]+$/u, "^[a-b-\\uE000]+$"],
1097 [/^[\d-]+$/u, "^[\\d-]+$"]
1098 ] as const
1099 ) {
1100 assertJsonSchemaDocument(Schema.String.check(Schema.isPattern(regExp)), {
1101 schema: { type: "string", pattern }
1102 })
1103 }
1104 })
1105
1106 it("omits isPattern without the Unicode flag", () => {
1107 for (
1108 const [regExp, values] of [
1109 [/^.$/, ["a", "😀", "\uD800"]],
1110 [/[\uD7FF-\uE000]/y, ["😀"]],
1111 [/^[\uD7FF-\uE000]{2}$/, ["😀"]]
1112 ] as const
1113 ) {
1114 const schema = Schema.String.check(Schema.isPattern(regExp))
1115 assertJsonSchemaDocument(schema, { schema: { type: "string" } })
1116 assertJsonSchemaAcceptsEffectValues(schema, values)
1117 }
1118 })
1119
1120 it("escapes regexp syntax in literal string checks", () => {
1121 for (
1122 const [check, pattern] of [
1123 [Schema.isStartingWith("a.b"), "^a\\.b"],
1124 [Schema.isEndingWith("a+b"), "a\\+b$"],
1125 [Schema.isIncluding("["), "\\["]
1126 ] as const
1127 ) {
1128 assertJsonSchemaDocument(Schema.String.check(check), {
1129 schema: {
1130 "type": "string",
1131 pattern
1132 }
1133 })
1134 }
1135 })
1136
1137 it("loosens literal patterns only at surrogate-pair boundaries", () => {
1138 for (
1139 const [check, pattern, values] of [
1140 [Schema.isStartingWith("abc\uD83D"), "^abc", ["abc😀"]],
1141 [Schema.isEndingWith("\uDE00xyz"), "xyz$", ["😀xyz"]],
1142 [Schema.isIncluding("\uDE00x\uD83D"), "x", ["😀x😀"]]
1143 ] as const
1144 ) {
1145 const schema = Schema.String.check(check)
1146 assertJsonSchemaDocument(schema, {
1147 schema: { type: "string", pattern }
1148 })
1149 assertJsonSchemaAcceptsEffectValues(schema, values)
1150 }
1151 })
1152
1153 it("does not use approximate literal patterns as record selectors", () => {
1154 const schema = Schema.StructWithRest(
1155 Schema.Struct({ abc: Schema.String }),
1156 [Schema.Record(Schema.String.check(Schema.isStartingWith("abc\uD83D")), Schema.Finite)]
1157 )
1158 assertJsonSchemaAcceptsEffectValues(schema, [{ abc: "text", "abc😀": 1 }])
1159 })
1160
1161 it("omits literal patterns whose entire constraint is a surrogate-pair boundary", () => {
1162 for (
1163 const [check, values] of [
1164 [Schema.isStartingWith("\uD83D"), ["😀"]],
1165 [Schema.isEndingWith("\uDE00"), ["😀"]],
1166 [Schema.isIncluding("\uDE00\uD83D"), ["😀😀"]]
1167 ] as const
1168 ) {
1169 const schema = Schema.String.check(check)
1170 assertJsonSchemaDocument(schema, { schema: { type: "string" } })
1171 assertJsonSchemaAcceptsEffectValues(schema, values)
1172 }
1173 })
1174
1175 it("isTrimmed", () => {
1176 const schema = Schema.Trimmed
1177 assertJsonSchemaDocument(schema, {
1178 schema: {
1179 "type": "string",
1180 "pattern": "^\\S[\\s\\S]*\\S$|^\\S$|^$"
1181 }
1182 })
1183 })
1184
1185 it("isLowercased", () => {
1186 const schema = Schema.String.check(Schema.isLowercased())
1187 assertJsonSchemaDocument(schema, {
1188 schema: {
1189 "type": "string",
1190 "pattern": "^[^A-Z]*$"
1191 }
1192 })
1193 })
1194
1195 it("isUppercased", () => {
1196 const schema = Schema.String.check(Schema.isUppercased())
1197 assertJsonSchemaDocument(schema, {
1198 schema: {
1199 "type": "string",
1200 "pattern": "^[^a-z]*$"
1201 }
1202 })
1203 })
1204
1205 it("isCapitalized", () => {
1206 const schema = Schema.String.check(Schema.isCapitalized())
1207 assertJsonSchemaDocument(schema, {
1208 schema: {
1209 "type": "string",
1210 "pattern": "^(?:[^a-z][\\s\\S]*)?$"
1211 }
1212 })
1213 })
1214
1215 it("isUncapitalized", () => {
1216 const schema = Schema.String.check(Schema.isUncapitalized())
1217 assertJsonSchemaDocument(schema, {
1218 schema: {
1219 "type": "string",
1220 "pattern": "^(?:[^A-Z][\\s\\S]*)?$"
1221 }
1222 })
1223 })
1224
1225 describe("isBetweenLength", () => {
1226 it("String", () => {
1227 assertJsonSchemaDocument(
1228 Schema.String.check(Schema.isBetweenLength(2, 2)),
1229 {
1230 schema: {
1231 "type": "string",
1232 "minLength": 1,
1233 "maxLength": 2
1234 }
1235 }
1236 )
1237 })
1238
1239 it("Array", () => {
1240 assertJsonSchemaDocument(
1241 Schema.Array(Schema.String).check(Schema.isBetweenLength(2, 2)),
1242 {
1243 schema: {
1244 "type": "array",
1245 "items": {
1246 "type": "string"
1247 },
1248 "minItems": 2,
1249 "maxItems": 2
1250 }
1251 }
1252 )
1253 })
1254
1255 it("NonEmptyArray", () => {
1256 assertJsonSchemaDocument(
1257 Schema.NonEmptyArray(Schema.String).check(Schema.isBetweenLength(2, 2)),
1258 {
1259 schema: {
1260 "type": "array",
1261 "prefixItems": [{
1262 "type": "string"
1263 }],
1264 "items": {
1265 "type": "string"
1266 },
1267 "minItems": 1,
1268 "allOf": [
1269 { "minItems": 2, "maxItems": 2 }
1270 ]
1271 }
1272 }
1273 )
1274 })
1275
1276 it("exports the tightest code-point bounds that accept every valid string", () => {
1277 const schema = Schema.String.check(Schema.isBetweenLength(3, 4))
1278 assertJsonSchemaDocument(schema, {
1279 schema: {
1280 type: "string",
1281 minLength: 2,
1282 maxLength: 4
1283 }
1284 })
1285 assertJsonSchemaAcceptsEffectValues(schema, ["ab", "a😀", "😀😀", "abcde"])
1286 })
1287 })
1288
1289 it("exports code point checks with matching JSON Schema validation", () => {
1290 for (
1291 const [check, keywords] of [
1292 [Schema.isMinCodePoints(2), { minLength: 2 }],
1293 [Schema.isMaxCodePoints(1), { maxLength: 1 }],
1294 [Schema.isBetweenCodePoints(1, 2), { allOf: [{ minLength: 1 }, { maxLength: 2 }] }]
1295 ] as const
1296 ) {
1297 const schema = Schema.String.check(check)
1298 const jsonSchema = { type: "string", ...keywords } as const
1299 assertJsonSchemaDocument(schema, { schema: jsonSchema })
1300 const validate = ajvDraft2020_12.compile(jsonSchema)
1301 for (const value of ["", "a", "😀", "😀a", "😀😀😀", "e\u0301", "\uD800", "\uDC00"]) {
1302 assert.strictEqual(validate(value), Schema.is(schema)(value))
1303 }
1304 }
1305 })
1306
1307 describe("isMinLength", () => {
1308 it("String", () => {
1309 assertJsonSchemaDocument(
1310 Schema.String.check(Schema.isMinLength(2)),
1311 {
1312 schema: {
1313 "type": "string",
1314 "minLength": 1
1315 }
1316 }
1317 )
1318 })
1319
1320 it("exports a loose lower bound for strings", () => {
1321 const schema = Schema.String.check(Schema.isMinLength(3))
1322 assertJsonSchemaDocument(schema, {
1323 schema: { type: "string", minLength: 2 }
1324 })
1325 assertJsonSchemaAcceptsEffectValues(schema, ["a😀", "😀😀", "ab"])
1326 })
1327
1328 it("exports both applicable bounds when the input type is a union", () => {
1329 const schema = Schema.Union([Schema.String, Schema.Array(Schema.String)]).check(Schema.isMinLength(2))
1330 assertJsonSchemaAcceptsEffectValues(schema, ["😀", ["a", "b"]])
1331 })
1332
1333 it("Array", () => {
1334 assertJsonSchemaDocument(
1335 Schema.Array(Schema.String).check(Schema.isMinLength(2)),
1336 {
1337 schema: {
1338 "type": "array",
1339 "items": {
1340 "type": "string"
1341 },
1342 "minItems": 2
1343 }
1344 }
1345 )
1346 })
1347
1348 it("NonEmptyArray", () => {
1349 assertJsonSchemaDocument(
1350 Schema.NonEmptyArray(Schema.String).check(Schema.isMinLength(2)),
1351 {
1352 schema: {
1353 "type": "array",
1354 "prefixItems": [{
1355 "type": "string"
1356 }],
1357 "items": {
1358 "type": "string"
1359 },
1360 "minItems": 1,
1361 "allOf": [
1362 { "minItems": 2 }
1363 ]
1364 }
1365 }
1366 )
1367 })
1368 })
1369
1370 describe("isMaxLength", () => {
1371 it("String", () => {
1372 assertJsonSchemaDocument(
1373 Schema.String.check(Schema.isMaxLength(2)),
1374 {
1375 schema: {
1376 "type": "string",
1377 "maxLength": 2
1378 }
1379 }
1380 )
1381 })
1382
1383 it("exports a loose upper bound for strings", () => {
1384 const schema = Schema.String.check(Schema.isMaxLength(2))
1385 assertJsonSchemaDocument(schema, {
1386 schema: { type: "string", maxLength: 2 }
1387 })
1388 assertJsonSchemaAcceptsEffectValues(schema, ["a", "😀", "😀😀"])
1389 })
1390
1391 it("Array", () => {
1392 assertJsonSchemaDocument(
1393 Schema.Array(Schema.String).check(Schema.isMaxLength(2)),
1394 {
1395 schema: {
1396 "type": "array",
1397 "items": {
1398 "type": "string"
1399 },
1400 "maxItems": 2
1401 }
1402 }
1403 )
1404 })
1405
1406 it("NonEmptyArray", () => {
1407 assertJsonSchemaDocument(
1408 Schema.NonEmptyArray(Schema.String).check(Schema.isMaxLength(2)),
1409 {
1410 schema: {
1411 "type": "array",
1412 "maxItems": 2,
1413 "minItems": 1,
1414 "prefixItems": [{
1415 "type": "string"
1416 }],
1417 "items": {
1418 "type": "string"
1419 }
1420 }
1421 }
1422 )
1423 })
1424 })
1425
1426 it("rejects non-finite cardinality bounds", () => {
1427 const unaryChecks: ReadonlyArray<(value: number) => unknown> = [
1428 Schema.isMinLength,
1429 Schema.isMaxLength,
1430 Schema.isMinCodePoints,
1431 Schema.isMaxCodePoints,
1432 Schema.isMinSize,
1433 Schema.isMaxSize,
1434 Schema.isMinProperties,
1435 Schema.isMaxProperties
1436 ]
1437 const binaryChecks: ReadonlyArray<(minimum: number, maximum: number) => unknown> = [
1438 Schema.isBetweenLength,
1439 Schema.isBetweenCodePoints,
1440 Schema.isBetweenSize,
1441 Schema.isBetweenProperties
1442 ]
1443 for (const [value, formatted] of [[NaN, "NaN"], [Infinity, "Infinity"], [-Infinity, "-Infinity"]] as const) {
1444 for (const check of unaryChecks) {
1445 throws(() => check(value), new RangeError(`Expected a finite number, got ${formatted}`))
1446 }
1447 for (const check of binaryChecks) {
1448 throws(() => check(value, 1), new RangeError(`Expected a finite number, got ${formatted}`))
1449 throws(() => check(1, value), new RangeError(`Expected a finite number, got ${formatted}`))
1450 }
1451 }
1452 })
1453
1454 it("isUUID", () => {
1455 assertJsonSchemaDocument(
1456 Schema.String.annotate({ description: "description" }).check(Schema.isUUID()),
1457 {
1458 schema: {
1459 "type": "string",
1460 "description": "description",
1461 "format": "uuid",
1462 "pattern":
1463 "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|[fF]{8}-[fF]{4}-[fF]{4}-[fF]{4}-[fF]{12})$"
1464 }
1465 }
1466 )
1467 })
1468
1469 it("isGUID", () => {
1470 assertJsonSchemaDocument(
1471 Schema.String.annotate({ description: "description" }).check(Schema.isGUID()),
1472 {
1473 schema: {
1474 "type": "string",
1475 "description": "description",
1476 "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$"
1477 }
1478 }
1479 )
1480 })
1481
1482 it("isBase64", () => {
1483 assertJsonSchemaDocument(
1484 Schema.String.annotate({ description: "description" }).check(Schema.isBase64()),
1485 {
1486 schema: {
1487 "type": "string",
1488 "description": "description",
1489 "pattern": "^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$"
1490 }
1491 }
1492 )
1493 })
1494
1495 it("isBase64Url", () => {
1496 assertJsonSchemaDocument(
1497 Schema.String.annotate({ description: "description" }).check(Schema.isBase64Url()),
1498 {
1499 schema: {
1500 "type": "string",
1501 "description": "description",
1502 "pattern": "^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$"
1503 }
1504 }
1505 )
1506 })
1507 })
1508 })
1509
1510 describe("Number", () => {
1511 it("Number", () => {
1512 const schema = Schema.Number
1513 assertJsonSchemaDocument(
1514 schema,
1515 {
1516 schema: {
1517 "anyOf": [
1518 { "type": "number" },
1519 { "type": "string", "enum": ["Infinity", "-Infinity", "NaN"] }
1520 ]
1521 }
1522 }
1523 )
1524 assertJsonSchemaDocument(
1525 schema.annotate({ description: "a" }),
1526 {
1527 schema: {
1528 "anyOf": [
1529 { "type": "number" },
1530 { "type": "string", "enum": ["Infinity", "-Infinity", "NaN"] }
1531 ]
1532 }
1533 }
1534 )
1535 })
1536
1537 it("Number & annotateKey", () => {
1538 assertJsonSchemaDocument(
1539 Schema.Struct({
1540 value: Schema.Number.annotateKey({ description: "the field" })
1541 }),
1542 {
1543 schema: {
1544 type: "object",
1545 properties: {
1546 value: {
1547 anyOf: [
1548 { type: "number" },
1549 { type: "string", enum: ["Infinity", "-Infinity", "NaN"] }
1550 ],
1551 allOf: [{ description: "the field" }]
1552 }
1553 },
1554 required: ["value"],
1555 additionalProperties: false
1556 }
1557 }
1558 )
1559 })
1560 })
1561
1562 describe("Finite", () => {
1563 it("Finite", () => {
1564 assertJsonSchemaDocument(
1565 Schema.Finite,
1566 {
1567 schema: {
1568 "type": "number"
1569 }
1570 }
1571 )
1572 assertJsonSchemaDocument(
1573 Schema.Finite.annotate({ description: "a" }),
1574 {
1575 schema: {
1576 "type": "number",
1577 "description": "a"
1578 }
1579 }
1580 )
1581 })
1582
1583 describe("checks", () => {
1584 it("isInt", () => {
1585 assertJsonSchemaDocument(
1586 Schema.Finite.check(Schema.isInt()),
1587 {
1588 schema: {
1589 "type": "integer"
1590 }
1591 }
1592 )
1593 })
1594
1595 it("isInt32", () => {
1596 assertJsonSchemaDocument(
1597 Schema.Finite.check(Schema.isInt32()),
1598 {
1599 schema: {
1600 "type": "integer",
1601 "allOf": [
1602 { "maximum": 2147483647, "minimum": -2147483648 }
1603 ]
1604 }
1605 }
1606 )
1607 })
1608
1609 it("isUint32", () => {
1610 assertJsonSchemaDocument(
1611 Schema.Finite.check(Schema.isUint32()),
1612 {
1613 schema: {
1614 "type": "integer",
1615 "allOf": [
1616 { "maximum": 4294967295, "minimum": 0 }
1617 ]
1618 }
1619 }
1620 )
1621 assertJsonSchemaDocument(
1622 Schema.Finite.check(Schema.isUint32({ description: "a" })),
1623 {
1624 schema: {
1625 "type": "integer",
1626 "allOf": [
1627 {
1628 "description": "a",
1629 "allOf": [
1630 { "maximum": 4294967295, "minimum": 0 }
1631 ]
1632 }
1633 ]
1634 }
1635 }
1636 )
1637 assertJsonSchemaDocument(
1638 Schema.Finite.check(
1639 Schema.isUint32({ description: "a" })
1640 ),
1641 {
1642 schema: {
1643 "type": "integer",
1644 "allOf": [
1645 {
1646 "description": "a",
1647 "allOf": [
1648 { "maximum": 4294967295, "minimum": 0 }
1649 ]
1650 }
1651 ]
1652 }
1653 }
1654 )
1655 })
1656
1657 it("isGreaterThan", () => {
1658 assertJsonSchemaDocument(
1659 Schema.Finite.check(Schema.isGreaterThan(1)),
1660 {
1661 schema: {
1662 "type": "number",
1663 "exclusiveMinimum": 1
1664 }
1665 }
1666 )
1667 })
1668
1669 it("isGreaterThanOrEqualTo", () => {
1670 assertJsonSchemaDocument(
1671 Schema.Finite.check(Schema.isGreaterThanOrEqualTo(1)),
1672 {
1673 schema: {
1674 "type": "number",
1675 "minimum": 1
1676 }
1677 }
1678 )
1679 })
1680
1681 it("isLessThan", () => {
1682 assertJsonSchemaDocument(Schema.Finite.check(Schema.isLessThan(1)), {
1683 schema: {
1684 "type": "number",
1685 "exclusiveMaximum": 1
1686 }
1687 })
1688 })
1689
1690 it("isLessThanOrEqualTo", () => {
1691 assertJsonSchemaDocument(Schema.Finite.check(Schema.isLessThanOrEqualTo(1)), {
1692 schema: {
1693 "type": "number",
1694 "maximum": 1
1695 }
1696 })
1697 })
1698
1699 it("isBetween", () => {
1700 assertJsonSchemaDocument(
1701 Schema.Finite.check(Schema.isBetween({ minimum: 1, maximum: 10 })),
1702 {
1703 schema: {
1704 "type": "number",
1705 "minimum": 1,
1706 "maximum": 10
1707 }
1708 }
1709 )
1710 assertJsonSchemaDocument(
1711 Schema.Finite.check(
1712 Schema.isBetween({ minimum: 1, maximum: 10, exclusiveMinimum: true })
1713 ),
1714 {
1715 schema: {
1716 "type": "number",
1717 "exclusiveMinimum": 1,
1718 "maximum": 10
1719 }
1720 }
1721 )
1722 assertJsonSchemaDocument(
1723 Schema.Finite.check(
1724 Schema.isBetween({ minimum: 1, maximum: 10, exclusiveMaximum: true })
1725 ),
1726 {
1727 schema: {
1728 "type": "number",
1729 "minimum": 1,
1730 "exclusiveMaximum": 10
1731 }
1732 }
1733 )
1734 assertJsonSchemaDocument(
1735 Schema.Finite.check(
1736 Schema.isBetween({ minimum: 1, maximum: 10, exclusiveMinimum: true, exclusiveMaximum: true })
1737 ),
1738 {
1739 schema: {
1740 "type": "number",
1741 "exclusiveMinimum": 1,
1742 "exclusiveMaximum": 10
1743 }
1744 }
1745 )
1746 })
1747
1748 it("isMultipleOf", () => {
1749 assertJsonSchemaDocument(
1750 Schema.Int.check(Schema.isMultipleOf(2)),
1751 {
1752 schema: {
1753 "type": "integer",
1754 "multipleOf": 2
1755 }
1756 }
1757 )
1758 assertJsonSchemaDocument(
1759 Schema.Int.check(Schema.isMultipleOf(-2)),
1760 {
1761 schema: {
1762 "type": "integer",
1763 "multipleOf": 2
1764 }
1765 }
1766 )
1767 })
1768 })
1769 })
1770
1771 it("Boolean", () => {
1772 const schema = Schema.Boolean
1773 assertJsonSchemaDocument(
1774 schema,
1775 {
1776 schema: {
1777 "type": "boolean"
1778 }
1779 }
1780 )
1781 assertJsonSchemaDocument(
1782 schema.annotate({ description: "a" }),
1783 {
1784 schema: {
1785 "type": "boolean",
1786 "description": "a"
1787 }
1788 }
1789 )
1790 })
1791
1792 it("ObjectKeyword", () => {
1793 const schema = Schema.ObjectKeyword
1794 assertJsonSchemaDocument(
1795 schema,
1796 {
1797 schema: { anyOf: [{ type: "array" }, { type: "object" }] }
1798 }
1799 )
1800 assertJsonSchemaDocument(
1801 schema.annotate({ description: "a" }),
1802 {
1803 schema: {
1804 "anyOf": [
1805 { "type": "array" },
1806 { "type": "object" }
1807 ]
1808 }
1809 }
1810 )
1811 })
1812
1813 describe("Literal", () => {
1814 it("string", () => {
1815 const schema = Schema.Literal("a")
1816 assertJsonSchemaDocument(
1817 schema,
1818 {
1819 schema: {
1820 "type": "string",
1821 "enum": ["a"]
1822 }
1823 }
1824 )
1825 assertJsonSchemaDocument(
1826 schema.annotate({ description: "a" }),
1827 {
1828 schema: {
1829 "type": "string",
1830 "enum": ["a"],
1831 "description": "a"
1832 }
1833 }
1834 )
1835 })
1836
1837 it("number", () => {
1838 const schema = Schema.Literal(1)
1839 assertJsonSchemaDocument(
1840 schema,
1841 {
1842 schema: {
1843 "type": "number",
1844 "enum": [1]
1845 }
1846 }
1847 )
1848 assertJsonSchemaDocument(
1849 schema.annotate({ description: "a" }),
1850 {
1851 schema: {
1852 "type": "number",
1853 "enum": [1],
1854 "description": "a"
1855 }
1856 }
1857 )
1858 })
1859
1860 it("boolean", () => {
1861 const schema = Schema.Literal(true)
1862 assertJsonSchemaDocument(
1863 schema,
1864 {
1865 schema: {
1866 "type": "boolean",
1867 "enum": [true]
1868 }
1869 }
1870 )
1871 assertJsonSchemaDocument(
1872 schema.annotate({ description: "a" }),
1873 {
1874 schema: {
1875 "type": "boolean",
1876 "enum": [true],
1877 "description": "a"
1878 }
1879 }
1880 )
1881 })
1882
1883 it("bigint", () => {
1884 const schema = Schema.Literal(1n)
1885 assertJsonSchemaDocument(
1886 schema,
1887 {
1888 schema: {
1889 "type": "string",
1890 "enum": ["1"]
1891 }
1892 }
1893 )
1894 assertJsonSchemaDocument(
1895 schema.annotate({ description: "a" }),
1896 {
1897 schema: {
1898 "type": "string",
1899 "enum": ["1"]
1900 }
1901 }
1902 )
1903 })
1904 })
1905
1906 describe("Literals", () => {
1907 it("empty literals", () => {
1908 const schema = Schema.Literals([])
1909 assertJsonSchemaDocument(
1910 schema,
1911 {
1912 schema: {
1913 "not": {}
1914 }
1915 }
1916 )
1917 assertJsonSchemaDocument(
1918 schema.annotate({ description: "a" }),
1919 {
1920 schema: {
1921 "not": {},
1922 "description": "a"
1923 }
1924 }
1925 )
1926 })
1927
1928 it("strings", () => {
1929 const schema = Schema.Literals(["a", "b"])
1930 assertJsonSchemaDocument(
1931 schema,
1932 {
1933 schema: {
1934 "type": "string",
1935 "enum": ["a", "b"]
1936 }
1937 }
1938 )
1939 assertJsonSchemaDocument(
1940 schema.annotate({ description: "a" }),
1941 {
1942 schema: {
1943 "type": "string",
1944 "enum": ["a", "b"],
1945 "description": "a"
1946 }
1947 }
1948 )
1949 })
1950
1951 it("numbers", () => {
1952 const schema = Schema.Literals([1, 2])
1953 assertJsonSchemaDocument(
1954 schema,
1955 {
1956 schema: {
1957 "type": "number",
1958 "enum": [1, 2]
1959 }
1960 }
1961 )
1962 assertJsonSchemaDocument(
1963 schema.annotate({ description: "a" }),
1964 {
1965 schema: {
1966 "type": "number",
1967 "enum": [1, 2],
1968 "description": "a"
1969 }
1970 }
1971 )
1972 })
1973
1974 it("booleans", () => {
1975 const schema = Schema.Literals([true, false])
1976 assertJsonSchemaDocument(
1977 schema,
1978 {
1979 schema: {
1980 "type": "boolean",
1981 "enum": [true, false]
1982 }
1983 }
1984 )
1985 assertJsonSchemaDocument(
1986 schema.annotate({ description: "a" }),
1987 {
1988 schema: {
1989 "type": "boolean",
1990 "enum": [true, false],
1991 "description": "a"
1992 }
1993 }
1994 )
1995 })
1996
1997 it("strings & numbers", () => {
1998 const schema = Schema.Literals(["a", 1])
1999 assertJsonSchemaDocument(
2000 schema,
2001 {
2002 schema: {
2003 "anyOf": [
2004 {
2005 "type": "string",
2006 "enum": ["a"]
2007 },
2008 {
2009 "type": "number",
2010 "enum": [1]
2011 }
2012 ]
2013 }
2014 }
2015 )
2016 assertJsonSchemaDocument(
2017 schema.annotate({ description: "a" }),
2018 {
2019 schema: {
2020 "anyOf": [
2021 {
2022 "type": "string",
2023 "enum": ["a"]
2024 },
2025 {
2026 "type": "number",
2027 "enum": [1]
2028 }
2029 ],
2030 "description": "a"
2031 }
2032 }
2033 )
2034 })
2035 })
2036
2037 describe("Union of literals", () => {
2038 it("strings", () => {
2039 const schema = Schema.Union([
2040 Schema.Literal("a"),
2041 Schema.Literal("b")
2042 ])
2043 assertJsonSchemaDocument(
2044 schema,
2045 {
2046 schema: {
2047 "type": "string",
2048 "enum": ["a", "b"]
2049 }
2050 }
2051 )
2052 const jsonAnnotations = {
2053 "title": "title",
2054 "description": "description",
2055 "default": "a" as const,
2056 "examples": ["a", "b"] as const
2057 }
2058 assertJsonSchemaDocument(
2059 schema.annotate({ ...jsonAnnotations }),
2060 {
2061 schema: {
2062 "type": "string",
2063 "enum": ["a", "b"],
2064 ...jsonAnnotations
2065 }
2066 }
2067 )
2068 })
2069
2070 it("nested literals", () => {
2071 const schema = Schema.Union([
2072 Schema.Literal("a"),
2073 Schema.Literals(["b", "c"])
2074 ])
2075 assertJsonSchemaDocument(
2076 schema,
2077 {
2078 schema: {
2079 "type": "string",
2080 "enum": ["a", "b", "c"]
2081 }
2082 }
2083 )
2084 })
2085
2086 it("strings & inner annotate", () => {
2087 const schema = Schema.Union([
2088 Schema.Literal("a"),
2089 Schema.Literal("b").annotate({ description: "b-description" })
2090 ])
2091 assertJsonSchemaDocument(
2092 schema,
2093 {
2094 schema: {
2095 "anyOf": [
2096 {
2097 "type": "string",
2098 "enum": ["a"]
2099 },
2100 {
2101 "type": "string",
2102 "enum": ["b"],
2103 "description": "b-description"
2104 }
2105 ]
2106 }
2107 }
2108 )
2109 const jsonAnnotations = {
2110 "title": "title",
2111 "description": "description",
2112 "default": "a" as const,
2113 "examples": ["a", "b"] as const
2114 }
2115 assertJsonSchemaDocument(
2116 schema.annotate({ ...jsonAnnotations }),
2117 {
2118 schema: {
2119 "anyOf": [
2120 {
2121 "type": "string",
2122 "enum": ["a"]
2123 },
2124 {
2125 "type": "string",
2126 "enum": ["b"],
2127 "description": "b-description"
2128 }
2129 ],
2130 ...jsonAnnotations
2131 }
2132 }
2133 )
2134 })
2135
2136 it("numbers", () => {
2137 const schema = Schema.Union([
2138 Schema.Literal(1),
2139 Schema.Literal(2)
2140 ])
2141 assertJsonSchemaDocument(
2142 schema,
2143 {
2144 schema: {
2145 "type": "number",
2146 "enum": [1, 2]
2147 }
2148 }
2149 )
2150 const jsonAnnotations = {
2151 "title": "title",
2152 "description": "description",
2153 "default": 1 as const,
2154 "examples": [1, 2] as const
2155 }
2156 assertJsonSchemaDocument(
2157 schema.annotate({ ...jsonAnnotations }),
2158 {
2159 schema: {
2160 "type": "number",
2161 "enum": [1, 2],
2162 ...jsonAnnotations
2163 }
2164 }
2165 )
2166 })
2167
2168 it("booleans", () => {
2169 const schema = Schema.Union([
2170 Schema.Literal(true),
2171 Schema.Literal(false)
2172 ])
2173 assertJsonSchemaDocument(
2174 schema,
2175 {
2176 schema: {
2177 "type": "boolean",
2178 "enum": [true, false]
2179 }
2180 }
2181 )
2182 const jsonAnnotations = {
2183 "title": "title",
2184 "description": "description",
2185 "default": true as const,
2186 "examples": [true, false] as const
2187 }
2188 assertJsonSchemaDocument(
2189 schema.annotate({ ...jsonAnnotations }),
2190 {
2191 schema: {
2192 "type": "boolean",
2193 "enum": [true, false],
2194 ...jsonAnnotations
2195 }
2196 }
2197 )
2198 })
2199
2200 it("strings & numbers", () => {
2201 const schema = Schema.Union([
2202 Schema.Literal("a"),
2203 Schema.Literal(1)
2204 ])
2205 assertJsonSchemaDocument(
2206 schema,
2207 {
2208 schema: {
2209 "anyOf": [
2210 {
2211 "type": "string",
2212 "enum": ["a"]
2213 },
2214 {
2215 "type": "number",
2216 "enum": [1]
2217 }
2218 ]
2219 }
2220 }
2221 )
2222 const jsonAnnotations = {
2223 "title": "title",
2224 "description": "description",
2225 "default": "a" as const,
2226 "examples": ["a", 1] as const
2227 }
2228 assertJsonSchemaDocument(
2229 schema.annotate({ ...jsonAnnotations }),
2230 {
2231 schema: {
2232 "anyOf": [
2233 {
2234 "type": "string",
2235 "enum": ["a"]
2236 },
2237 {
2238 "type": "number",
2239 "enum": [1]
2240 }
2241 ],
2242 ...jsonAnnotations
2243 }
2244 }
2245 )
2246 })
2247 })
2248
2249 describe("Enum", () => {
2250 it("empty enum", () => {
2251 enum Empty {}
2252 const schema = Schema.Enum(Empty)
2253 assertJsonSchemaDocument(
2254 schema,
2255 {
2256 schema: {
2257 "not": {}
2258 }
2259 }
2260 )
2261 assertJsonSchemaDocument(
2262 schema.annotate({ description: "a" }),
2263 {
2264 schema: {
2265 "not": {},
2266 "description": "a"
2267 }
2268 }
2269 )
2270 })
2271
2272 it("single enum", () => {
2273 enum Fruits {
2274 Apple
2275 }
2276 const schema = Schema.Enum(Fruits)
2277 assertJsonSchemaDocument(
2278 schema,
2279 {
2280 schema: {
2281 "anyOf": [
2282 {
2283 "type": "number",
2284 "enum": [0],
2285 "title": "Apple"
2286 }
2287 ]
2288 }
2289 }
2290 )
2291 assertJsonSchemaDocument(
2292 schema.annotate({ description: "a" }),
2293 {
2294 schema: {
2295 "description": "a",
2296 "anyOf": [
2297 {
2298 "type": "number",
2299 "enum": [0],
2300 "title": "Apple"
2301 }
2302 ]
2303 }
2304 }
2305 )
2306 })
2307
2308 it("mixed enums (number & string)", () => {
2309 enum Fruits {
2310 Apple,
2311 Banana,
2312 Orange = "orange"
2313 }
2314 const schema = Schema.Enum(Fruits)
2315 assertJsonSchemaDocument(
2316 schema,
2317 {
2318 schema: {
2319 "anyOf": [
2320 {
2321 "type": "number",
2322 "enum": [0],
2323 "title": "Apple"
2324 },
2325 {
2326 "type": "number",
2327 "enum": [1],
2328 "title": "Banana"
2329 },
2330 {
2331 "type": "string",
2332 "enum": ["orange"],
2333 "title": "Orange"
2334 }
2335 ]
2336 }
2337 }
2338 )
2339 assertJsonSchemaDocument(
2340 schema.annotate({ description: "a" }),
2341 {
2342 schema: {
2343 "anyOf": [
2344 {
2345 "type": "number",
2346 "enum": [0],
2347 "title": "Apple"
2348 },
2349 {
2350 "type": "number",
2351 "enum": [1],
2352 "title": "Banana"
2353 },
2354 {
2355 "type": "string",
2356 "enum": ["orange"],
2357 "title": "Orange"
2358 }
2359 ],
2360 "description": "a"
2361 }
2362 }
2363 )
2364 })
2365
2366 it("const enum", () => {
2367 const Fruits = {
2368 Apple: "apple",
2369 Banana: "banana",
2370 Cantaloupe: 3
2371 } as const
2372 const schema = Schema.Enum(Fruits)
2373 assertJsonSchemaDocument(
2374 schema,
2375 {
2376 schema: {
2377 "anyOf": [
2378 {
2379 "type": "string",
2380 "title": "Apple",
2381 "enum": ["apple"]
2382 },
2383 {
2384 "type": "string",
2385 "title": "Banana",
2386 "enum": ["banana"]
2387 },
2388 {
2389 "type": "number",
2390 "title": "Cantaloupe",
2391 "enum": [3]
2392 }
2393 ]
2394 }
2395 }
2396 )
2397 })
2398 })
2399
2400 it("TemplateLiteral", () => {
2401 const schema = Schema.TemplateLiteral(["a", Schema.String])
2402 assertJsonSchemaDocument(schema, {
2403 schema: {
2404 "type": "string",
2405 "pattern": "^a[\\s\\S]*?$"
2406 }
2407 })
2408 assertJsonSchemaDocument(schema.annotate({ description: "a" }), {
2409 schema: {
2410 "type": "string",
2411 "pattern": "^a[\\s\\S]*?$",
2412 "description": "a"
2413 }
2414 })
2415 })
2416
2417 describe("Struct", () => {
2418 it("empty struct", () => {
2419 const schema = Schema.Struct({})
2420 assertJsonSchemaDocument(
2421 schema,
2422 {
2423 schema: {
2424 "not": { "type": "null" }
2425 }
2426 }
2427 )
2428 assertJsonSchemaDocument(
2429 schema.annotate({ description: "a" }),
2430 {
2431 schema: {
2432 "not": { "type": "null" },
2433 "description": "a"
2434 }
2435 }
2436 )
2437 })
2438
2439 describe("required property", () => {
2440 it("String", () => {
2441 assertJsonSchemaDocument(
2442 Schema.Struct({
2443 a: Schema.String
2444 }),
2445 {
2446 schema: {
2447 "type": "object",
2448 "properties": {
2449 "a": {
2450 "type": "string"
2451 }
2452 },
2453 "required": ["a"],
2454 "additionalProperties": false
2455 }
2456 }
2457 )
2458 })
2459
2460 it("String & annotate", () => {
2461 assertJsonSchemaDocument(
2462 Schema.Struct({
2463 a: Schema.String.annotate({ description: "a" })
2464 }),
2465 {
2466 schema: {
2467 "type": "object",
2468 "properties": {
2469 "a": {
2470 "type": "string",
2471 "description": "a"
2472 }
2473 },
2474 "required": ["a"],
2475 "additionalProperties": false
2476 }
2477 }
2478 )
2479 })
2480
2481 it("String & annotateKey", () => {
2482 assertJsonSchemaDocument(
2483 Schema.Struct({
2484 a: Schema.String.annotateKey({ description: "a-key" })
2485 }),
2486 {
2487 schema: {
2488 "type": "object",
2489 "properties": {
2490 "a": {
2491 "type": "string",
2492 "allOf": [{
2493 "description": "a-key"
2494 }]
2495 }
2496 },
2497 "required": ["a"],
2498 "additionalProperties": false
2499 }
2500 }
2501 )
2502 })
2503
2504 it("String & annotate & annotateKey", () => {
2505 assertJsonSchemaDocument(
2506 Schema.Struct({
2507 a: Schema.String.annotate({ description: "a" }).annotateKey({ description: "a-key" })
2508 }),
2509 {
2510 schema: {
2511 "type": "object",
2512 "properties": {
2513 "a": {
2514 "type": "string",
2515 "description": "a",
2516 "allOf": [{
2517 "description": "a-key"
2518 }]
2519 }
2520 },
2521 "required": ["a"],
2522 "additionalProperties": false
2523 }
2524 }
2525 )
2526 })
2527 })
2528
2529 describe("optionalKey", () => {
2530 it("String", () => {
2531 assertJsonSchemaDocument(
2532 Schema.Struct({
2533 a: Schema.optionalKey(Schema.String)
2534 }),
2535 {
2536 schema: {
2537 "type": "object",
2538 "properties": {
2539 "a": { "type": "string" }
2540 },
2541 "additionalProperties": false
2542 }
2543 }
2544 )
2545 })
2546
2547 it("String & annotate", () => {
2548 assertJsonSchemaDocument(
2549 Schema.Struct({
2550 a: Schema.optionalKey(Schema.String.annotate({ description: "a" })),
2551 b: Schema.optionalKey(Schema.String).annotate({ description: "b" })
2552 }),
2553 {
2554 schema: {
2555 "type": "object",
2556 "properties": {
2557 "a": {
2558 "type": "string",
2559 "description": "a"
2560 },
2561 "b": {
2562 "type": "string",
2563 "description": "b"
2564 }
2565 },
2566 "additionalProperties": false
2567 }
2568 }
2569 )
2570 })
2571
2572 it("String & annotateKey", () => {
2573 assertJsonSchemaDocument(
2574 Schema.Struct({
2575 a: Schema.optionalKey(Schema.String.annotateKey({ description: "a-key" })),
2576 b: Schema.optionalKey(Schema.String).annotateKey({ description: "b-key" })
2577 }),
2578 {
2579 schema: {
2580 "type": "object",
2581 "properties": {
2582 "a": {
2583 "type": "string",
2584 "allOf": [{
2585 "description": "a-key"
2586 }]
2587 },
2588 "b": {
2589 "type": "string",
2590 "allOf": [{
2591 "description": "b-key"
2592 }]
2593 }
2594 },
2595 "additionalProperties": false
2596 }
2597 }
2598 )
2599 })
2600
2601 it("String & annotate & annotateKey", () => {
2602 assertJsonSchemaDocument(
2603 Schema.Struct({
2604 a: Schema.optionalKey(Schema.String.annotate({ description: "a" }).annotateKey({ description: "a-key" })),
2605 b: Schema.optionalKey(Schema.String).annotate({ description: "b" }).annotateKey({ description: "b-key" }),
2606 c: Schema.optionalKey(Schema.String.annotate({ description: "a" }).annotateKey({ description: "a-key" }))
2607 .annotate({ description: "c-outer" }).annotateKey({ description: "c-outer-key" })
2608 }),
2609 {
2610 schema: {
2611 "type": "object",
2612 "properties": {
2613 "a": {
2614 "type": "string",
2615 "description": "a",
2616 "allOf": [{
2617 "description": "a-key"
2618 }]
2619 },
2620 "b": {
2621 "type": "string",
2622 "description": "b",
2623 "allOf": [{
2624 "description": "b-key"
2625 }]
2626 },
2627 "c": {
2628 "type": "string",
2629 "description": "c-outer",
2630 "allOf": [{
2631 "description": "c-outer-key"
2632 }]
2633 }
2634 },
2635 "additionalProperties": false
2636 }
2637 }
2638 )
2639 })
2640
2641 it("optionalKey(String) to String", () => {
2642 assertJsonSchemaDocument(
2643 Schema.Struct({
2644 a: Schema.optionalKey(Schema.String).pipe(Schema.encodeTo(Schema.String, {
2645 decode: SchemaGetter.passthrough(),
2646 encode: SchemaGetter.withDefault(Effect.succeed(""))
2647 }))
2648 }),
2649 {
2650 schema: {
2651 "type": "object",
2652 "properties": {
2653 "a": {
2654 "type": "string"
2655 }
2656 },
2657 "required": ["a"],
2658 "additionalProperties": false
2659 }
2660 }
2661 )
2662 })
2663 })
2664
2665 describe("optional", () => {
2666 it("String", () => {
2667 assertJsonSchemaDocument(
2668 Schema.Struct({
2669 a: Schema.optional(Schema.String)
2670 }),
2671 {
2672 schema: {
2673 "type": "object",
2674 "properties": {
2675 "a": {
2676 "anyOf": [
2677 { "type": "string" },
2678 { "type": "null" }
2679 ]
2680 }
2681 },
2682 "additionalProperties": false
2683 }
2684 }
2685 )
2686 })
2687
2688 it("String & annotate", () => {
2689 assertJsonSchemaDocument(
2690 Schema.Struct({
2691 a: Schema.optional(Schema.String.annotate({ description: "a" })),
2692 b: Schema.optional(Schema.String).annotate({ description: "b" })
2693 }),
2694 {
2695 schema: {
2696 "type": "object",
2697 "properties": {
2698 "a": {
2699 "anyOf": [
2700 { "type": "string", "description": "a" },
2701 { "type": "null" }
2702 ]
2703 },
2704 "b": {
2705 "anyOf": [
2706 { "type": "string" },
2707 { "type": "null" }
2708 ],
2709 "description": "b"
2710 }
2711 },
2712 "additionalProperties": false
2713 }
2714 }
2715 )
2716 })
2717
2718 it("String & annotateKey", () => {
2719 assertJsonSchemaDocument(
2720 Schema.Struct({
2721 a: Schema.optional(Schema.String).annotateKey({ description: "a-key" }),
2722 b: Schema.optional(Schema.String.annotate({ description: "b" })).annotateKey({ description: "b-key" })
2723 }),
2724 {
2725 schema: {
2726 "type": "object",
2727 "properties": {
2728 "a": {
2729 "anyOf": [
2730 { "type": "string" },
2731 { "type": "null" }
2732 ],
2733 "allOf": [{
2734 "description": "a-key"
2735 }]
2736 },
2737 "b": {
2738 "anyOf": [
2739 { "type": "string", "description": "b" },
2740 { "type": "null" }
2741 ],
2742 "allOf": [{
2743 "description": "b-key"
2744 }]
2745 }
2746 },
2747 "additionalProperties": false
2748 }
2749 }
2750 )
2751 })
2752
2753 it("optional(String) to String", () => {
2754 assertJsonSchemaDocument(
2755 Schema.Struct({
2756 a: Schema.optional(Schema.String).pipe(Schema.encodeTo(Schema.String, {
2757 decode: SchemaGetter.passthrough(),
2758 encode: SchemaGetter.withDefault(Effect.succeed(""))
2759 }))
2760 }),
2761 {
2762 schema: {
2763 "type": "object",
2764 "properties": {
2765 "a": {
2766 "type": "string"
2767 }
2768 },
2769 "required": ["a"],
2770 "additionalProperties": false
2771 }
2772 }
2773 )
2774 })
2775 })
2776
2777 describe("UndefinedOr", () => {
2778 it("String", () => {
2779 assertJsonSchemaDocument(
2780 Schema.Struct({
2781 a: Schema.UndefinedOr(Schema.String)
2782 }),
2783 {
2784 schema: {
2785 "type": "object",
2786 "properties": {
2787 "a": {
2788 "anyOf": [
2789 { "type": "string" },
2790 { "type": "null" }
2791 ]
2792 }
2793 },
2794 "required": ["a"],
2795 "additionalProperties": false
2796 }
2797 }
2798 )
2799 })
2800
2801 it("String & annotate", () => {
2802 assertJsonSchemaDocument(
2803 Schema.Struct({
2804 a: Schema.UndefinedOr(Schema.String.annotate({ description: "a" })),
2805 b: Schema.UndefinedOr(Schema.String).annotate({ description: "b" })
2806 }),
2807 {
2808 schema: {
2809 "type": "object",
2810 "properties": {
2811 "a": {
2812 "anyOf": [
2813 { "type": "string", "description": "a" },
2814 { "type": "null" }
2815 ]
2816 },
2817 "b": {
2818 "anyOf": [
2819 { "type": "string" },
2820 { "type": "null" }
2821 ],
2822 "description": "b"
2823 }
2824 },
2825 "required": ["a", "b"],
2826 "additionalProperties": false
2827 }
2828 }
2829 )
2830 })
2831
2832 it("String & annotateKey", () => {
2833 assertJsonSchemaDocument(
2834 Schema.Struct({
2835 a: Schema.UndefinedOr(Schema.String).annotateKey({ description: "a-key" }),
2836 b: Schema.UndefinedOr(Schema.String.annotate({ description: "b" })).annotateKey({ description: "b-key" })
2837 }),
2838 {
2839 schema: {
2840 "type": "object",
2841 "properties": {
2842 "a": {
2843 "anyOf": [
2844 { "type": "string" },
2845 { "type": "null" }
2846 ],
2847 "allOf": [{
2848 "description": "a-key"
2849 }]
2850 },
2851 "b": {
2852 "anyOf": [
2853 { "type": "string", "description": "b" },
2854 { "type": "null" }
2855 ],
2856 "allOf": [{
2857 "description": "b-key"
2858 }]
2859 }
2860 },
2861 "required": ["a", "b"],
2862 "additionalProperties": false
2863 }
2864 }
2865 )
2866 })
2867
2868 it("UndefinedOr(String) to String", () => {
2869 assertJsonSchemaDocument(
2870 Schema.Struct({
2871 a: Schema.UndefinedOr(Schema.String).pipe(Schema.encodeTo(Schema.String, {
2872 decode: SchemaGetter.passthrough(),
2873 encode: SchemaGetter.transform((s) => s ?? "")
2874 }))
2875 }),
2876 {
2877 schema: {
2878 "type": "object",
2879 "properties": {
2880 "a": {
2881 "type": "string"
2882 }
2883 },
2884 "required": ["a"],
2885 "additionalProperties": false
2886 }
2887 }
2888 )
2889 })
2890 })
2891 })
2892
2893 describe("Record", () => {
2894 it("Record(String, Never)", () => {
2895 assertJsonSchemaDocument(
2896 Schema.Record(Schema.String, Schema.Never),
2897 {
2898 schema: {
2899 "type": "object",
2900 "additionalProperties": false
2901 }
2902 }
2903 )
2904 assertJsonSchemaDocument(
2905 Schema.Record(Schema.String, Schema.Never.annotate({ description: "a" })),
2906 {
2907 schema: {
2908 "type": "object",
2909 "additionalProperties": {
2910 "description": "a",
2911 "not": {}
2912 }
2913 }
2914 }
2915 )
2916 })
2917
2918 it("Record(String, Struct({}))", () => {
2919 assertJsonSchemaDocument(
2920 Schema.Record(Schema.String, Schema.Struct({})),
2921 {
2922 schema: {
2923 type: "object",
2924 additionalProperties: { not: { type: "null" } }
2925 }
2926 }
2927 )
2928 })
2929
2930 it("Record(isStartingWith, Struct({}))", () => {
2931 assertJsonSchemaDocument(
2932 Schema.Record(Schema.String.check(Schema.isStartingWith("a")), Schema.Struct({})),
2933 {
2934 schema: {
2935 type: "object",
2936 patternProperties: {
2937 "^a": { not: { type: "null" } }
2938 },
2939 additionalProperties: false
2940 }
2941 }
2942 )
2943 })
2944
2945 it("Record(String, Finite)", () => {
2946 assertJsonSchemaDocument(
2947 Schema.Record(Schema.String, Schema.Finite),
2948 {
2949 schema: {
2950 "type": "object",
2951 "additionalProperties": {
2952 "type": "number"
2953 }
2954 }
2955 }
2956 )
2957 assertJsonSchemaDocument(
2958 Schema.Record(
2959 Schema.String.annotate({ description: "k" }), // TODO: where can we attach the description?
2960 Schema.Finite.annotate({ description: "v" })
2961 ).annotate({ description: "r" }),
2962 {
2963 schema: {
2964 "type": "object",
2965 "additionalProperties": {
2966 "type": "number",
2967 "description": "v"
2968 },
2969 "description": "r"
2970 }
2971 }
2972 )
2973 })
2974
2975 it("JsonObject", () => {
2976 const schema = Schema.JsonObject
2977 assertJsonSchemaDocument(
2978 schema,
2979 {
2980 schema: { "type": "object" }
2981 }
2982 )
2983 })
2984
2985 it("Record(`a${string}`, Number) & annotate", () => {
2986 assertJsonSchemaDocument(
2987 Schema.Record(Schema.TemplateLiteral(["a", Schema.String]), Schema.Finite),
2988 {
2989 schema: {
2990 "type": "object",
2991 "patternProperties": {
2992 "^a[\\s\\S]*?$": {
2993 "type": "number"
2994 }
2995 },
2996 "additionalProperties": false
2997 }
2998 }
2999 )
3000 })
3001
3002 it("Record(Literals(['a', 'b']), Finite)", () => {
3003 assertJsonSchemaDocument(
3004 Schema.Record(Schema.Literals(["a", "b"]), Schema.Finite),
3005 {
3006 schema: {
3007 "type": "object",
3008 "properties": {
3009 "a": { "type": "number" },
3010 "b": { "type": "number" }
3011 },
3012 "required": ["a", "b"],
3013 "additionalProperties": false
3014 }
3015 }
3016 )
3017 })
3018
3019 it("Record(isUppercased, Number)", () => {
3020 assertJsonSchemaDocument(
3021 Schema.Record(Schema.String.check(Schema.isUppercased()), Schema.Finite),
3022 {
3023 schema: {
3024 "type": "object",
3025 "propertyNames": {
3026 "type": "string",
3027 "pattern": "^[^a-z]*$"
3028 },
3029 "additionalProperties": { "type": "number" }
3030 }
3031 }
3032 )
3033 })
3034
3035 it("uses propertyNames for conjunctive record key patterns", () => {
3036 assertJsonSchemaDocument(
3037 Schema.Record(Schema.String.check(Schema.isPattern(/^ab/u)), Schema.Finite),
3038 {
3039 schema: {
3040 type: "object",
3041 patternProperties: {
3042 "^ab": { type: "number" }
3043 },
3044 additionalProperties: false
3045 }
3046 }
3047 )
3048 assertJsonSchemaDocument(
3049 Schema.Record(
3050 Schema.String.check(Schema.isPattern(/^ab/u), Schema.isEndingWith("z")),
3051 Schema.Finite
3052 ),
3053 {
3054 schema: {
3055 type: "object",
3056 propertyNames: {
3057 type: "string",
3058 pattern: "^ab",
3059 allOf: [{ pattern: "z$" }]
3060 },
3061 additionalProperties: { type: "number" }
3062 }
3063 }
3064 )
3065 assertJsonSchemaDocument(
3066 Schema.Record(
3067 Schema.String.check(Schema.isStartingWith("A"), Schema.isUppercased()),
3068 Schema.Finite
3069 ),
3070 {
3071 schema: {
3072 type: "object",
3073 propertyNames: {
3074 type: "string",
3075 pattern: "^A",
3076 allOf: [{ pattern: "^[^a-z]*$" }]
3077 },
3078 additionalProperties: { type: "number" }
3079 }
3080 }
3081 )
3082 })
3083
3084 it("does not use a partial pattern as an index selector", () => {
3085 const schema = Schema.Record(
3086 Schema.String.check(Schema.isStartingWith("x"), Schema.isMinLength(3)),
3087 Schema.Finite
3088 )
3089 assertJsonSchemaDocument(
3090 schema,
3091 {
3092 schema: {
3093 type: "object",
3094 additionalProperties: true
3095 }
3096 },
3097 { onExcessProperty: "ignore" }
3098 )
3099 assertJsonSchemaDocument(schema, {
3100 schema: {
3101 type: "object",
3102 propertyNames: {
3103 type: "string",
3104 pattern: "^x",
3105 minLength: 2
3106 },
3107 additionalProperties: { type: "number" }
3108 }
3109 })
3110 })
3111
3112 describe("checks", () => {
3113 it("isMinProperties", () => {
3114 assertJsonSchemaDocument(
3115 Schema.Record(Schema.String, Schema.Finite).check(Schema.isMinProperties(2)),
3116 {
3117 schema: {
3118 "type": "object",
3119 "additionalProperties": {
3120 "type": "number"
3121 },
3122 "minProperties": 2
3123 }
3124 }
3125 )
3126 })
3127
3128 it("isMaxProperties", () => {
3129 assertJsonSchemaDocument(
3130 Schema.Record(Schema.String, Schema.Finite).check(Schema.isMaxProperties(2)),
3131 {
3132 schema: {
3133 "type": "object",
3134 "additionalProperties": { "type": "number" },
3135 "maxProperties": 2
3136 }
3137 }
3138 )
3139 })
3140
3141 it("isBetweenProperties", () => {
3142 assertJsonSchemaDocument(
3143 Schema.Record(Schema.String, Schema.Finite).check(Schema.isBetweenProperties(2, 2)),
3144 {
3145 schema: {
3146 "type": "object",
3147 "additionalProperties": { "type": "number" },
3148 "minProperties": 2,
3149 "maxProperties": 2
3150 }
3151 }
3152 )
3153 })
3154 })
3155 })
3156
3157 describe("StructWithRest", () => {
3158 it("property and string index", () => {
3159 assertJsonSchemaDocument(
3160 Schema.StructWithRest(Schema.Struct({ a: Schema.String }), [
3161 Schema.Record(Schema.String, Schema.Union([Schema.Finite, Schema.String]))
3162 ]),
3163 {
3164 schema: {
3165 "type": "object",
3166 "properties": {
3167 "a": { "type": "string" }
3168 },
3169 "allOf": [{
3170 "type": "object",
3171 "additionalProperties": {
3172 "anyOf": [
3173 { "type": "number" },
3174 { "type": "string" }
3175 ]
3176 }
3177 }],
3178 "required": ["a"]
3179 }
3180 }
3181 )
3182 })
3183
3184 it("pattern and string indexes", () => {
3185 assertJsonSchemaDocument(
3186 Schema.StructWithRest(Schema.Struct({}), [
3187 Schema.Record(Schema.String.check(Schema.isUppercased()), Schema.Finite),
3188 Schema.Record(Schema.String, Schema.Boolean)
3189 ]),
3190 {
3191 schema: {
3192 type: "object",
3193 propertyNames: {
3194 anyOf: [
3195 { type: "string", pattern: "^[^a-z]*$" },
3196 { type: "string" }
3197 ]
3198 },
3199 additionalProperties: {
3200 anyOf: [{ type: "number" }, { type: "boolean" }]
3201 },
3202 allOf: [{ type: "object", additionalProperties: { type: "boolean" } }]
3203 }
3204 }
3205 )
3206 })
3207
3208 it("property and compound pattern index", () => {
3209 assertJsonSchemaDocument(
3210 Schema.StructWithRest(Schema.Struct({ a: Schema.String }), [
3211 Schema.Record(
3212 Schema.String.check(Schema.isStartingWith("x"), Schema.isEndingWith("z")),
3213 Schema.Finite
3214 )
3215 ]),
3216 {
3217 schema: {
3218 type: "object",
3219 properties: {
3220 a: { type: "string" }
3221 },
3222 required: ["a"],
3223 propertyNames: {
3224 anyOf: [
3225 { enum: ["a"] },
3226 {
3227 type: "string",
3228 pattern: "^x",
3229 allOf: [{ pattern: "z$" }]
3230 }
3231 ]
3232 },
3233 additionalProperties: { type: "number" }
3234 }
3235 }
3236 )
3237 })
3238 })
3239
3240 describe("Tuple", () => {
3241 it("empty tuple", () => {
3242 const schema = Schema.Tuple([])
3243 assertJsonSchemaDocument(
3244 schema,
3245 {
3246 schema: {
3247 "type": "array",
3248 "items": false
3249 }
3250 }
3251 )
3252 assertJsonSchemaDocument(
3253 schema.annotate({ description: "a" }),
3254 {
3255 schema: {
3256 "type": "array",
3257 "items": false,
3258 "description": "a"
3259 }
3260 }
3261 )
3262 })
3263
3264 describe("required element", () => {
3265 it("String", () => {
3266 assertJsonSchemaDocument(
3267 Schema.Tuple([
3268 Schema.String
3269 ]),
3270 {
3271 schema: {
3272 "type": "array",
3273 "prefixItems": [{ "type": "string" }],
3274 "minItems": 1,
3275 "maxItems": 1
3276 }
3277 }
3278 )
3279 })
3280
3281 it("String & annotate", () => {
3282 assertJsonSchemaDocument(
3283 Schema.Tuple([
3284 Schema.String.annotate({ description: "a" })
3285 ]),
3286 {
3287 schema: {
3288 "type": "array",
3289 "prefixItems": [{ "type": "string", "description": "a" }],
3290 "minItems": 1,
3291 "maxItems": 1
3292 }
3293 }
3294 )
3295 })
3296
3297 it("String & annotateKey", () => {
3298 assertJsonSchemaDocument(
3299 Schema.Tuple([
3300 Schema.String.annotateKey({ description: "a-key" })
3301 ]),
3302 {
3303 schema: {
3304 "type": "array",
3305 "prefixItems": [{ "type": "string", "allOf": [{ "description": "a-key" }] }],
3306 "minItems": 1,
3307 "maxItems": 1
3308 }
3309 }
3310 )
3311 })
3312
3313 it("String & annotate & annotateKey", () => {
3314 assertJsonSchemaDocument(
3315 Schema.Tuple([
3316 Schema.String.annotate({ description: "a" }).annotateKey({ description: "a-key" })
3317 ]),
3318 {
3319 schema: {
3320 "type": "array",
3321 "prefixItems": [{ "type": "string", "description": "a", "allOf": [{ "description": "a-key" }] }],
3322 "minItems": 1,
3323 "maxItems": 1
3324 }
3325 }
3326 )
3327 })
3328 })
3329
3330 describe("optionalKey", () => {
3331 it("String", () => {
3332 assertJsonSchemaDocument(
3333 Schema.Tuple([
3334 Schema.optionalKey(Schema.String)
3335 ]),
3336 {
3337 schema: {
3338 "type": "array",
3339 "prefixItems": [
3340 { "type": "string" }
3341 ],
3342 "maxItems": 1
3343 }
3344 }
3345 )
3346 })
3347
3348 it("String & annotate", () => {
3349 assertJsonSchemaDocument(
3350 Schema.Tuple([
3351 Schema.optionalKey(Schema.String.annotate({ description: "a" })),
3352 Schema.optionalKey(Schema.String).annotate({ description: "b" })
3353 ]),
3354 {
3355 schema: {
3356 "type": "array",
3357 "prefixItems": [
3358 { "type": "string", "description": "a" },
3359 { "type": "string", "description": "b" }
3360 ],
3361 "maxItems": 2
3362 }
3363 }
3364 )
3365 })
3366
3367 it("String & annotateKey", () => {
3368 assertJsonSchemaDocument(
3369 Schema.Tuple([
3370 Schema.optionalKey(Schema.String.annotateKey({ description: "a-key" })),
3371 Schema.optionalKey(Schema.String).annotateKey({ description: "b-key" })
3372 ]),
3373 {
3374 schema: {
3375 "type": "array",
3376 "prefixItems": [
3377 { "type": "string", "allOf": [{ "description": "a-key" }] },
3378 {
3379 "type": "string",
3380 "allOf": [{ "description": "b-key" }]
3381 }
3382 ],
3383 "maxItems": 2
3384 }
3385 }
3386 )
3387 })
3388
3389 it("String & annotate & annotateKey", () => {
3390 assertJsonSchemaDocument(
3391 Schema.Tuple([
3392 Schema.optionalKey(Schema.String.annotate({ description: "a" }).annotateKey({ description: "a-key" })),
3393 Schema.optionalKey(Schema.String).annotate({ description: "b" }).annotateKey({ description: "b-key" }),
3394 Schema.optionalKey(Schema.String.annotate({ description: "a" }).annotateKey({ description: "a-key" }))
3395 .annotate({ description: "c-outer" }).annotateKey({ description: "c-outer-key" })
3396 ]),
3397 {
3398 schema: {
3399 "type": "array",
3400 "prefixItems": [
3401 { "type": "string", "description": "a", "allOf": [{ "description": "a-key" }] },
3402 {
3403 "type": "string",
3404 "description": "b",
3405 "allOf": [{ "description": "b-key" }]
3406 },
3407 { "type": "string", "description": "c-outer", "allOf": [{ "description": "c-outer-key" }] }
3408 ],
3409 "maxItems": 3
3410 }
3411 }
3412 )
3413 })
3414
3415 it("optionalKey(String) to String", () => {
3416 assertJsonSchemaDocument(
3417 Schema.Tuple([
3418 Schema.optionalKey(Schema.String).pipe(Schema.encodeTo(Schema.String, {
3419 decode: SchemaGetter.passthrough(),
3420 encode: SchemaGetter.withDefault(Effect.succeed(""))
3421 }))
3422 ]),
3423 {
3424 schema: {
3425 "type": "array",
3426 "prefixItems": [{ "type": "string" }],
3427 "minItems": 1,
3428 "maxItems": 1
3429 }
3430 }
3431 )
3432 })
3433 })
3434
3435 it("optionalKey to required key", () => {
3436 assertJsonSchemaDocument(
3437 Schema.Tuple([
3438 Schema.optionalKey(Schema.String).pipe(Schema.encodeTo(Schema.String, {
3439 decode: SchemaGetter.passthrough(),
3440 encode: SchemaGetter.withDefault(Effect.succeed(""))
3441 }))
3442 ]),
3443 {
3444 schema: {
3445 "type": "array",
3446 "prefixItems": [
3447 { "type": "string" }
3448 ],
3449 "minItems": 1,
3450 "maxItems": 1
3451 }
3452 }
3453 )
3454 })
3455 })
3456
3457 describe("Array", () => {
3458 it("Array(String)", () => {
3459 assertJsonSchemaDocument(
3460 Schema.Array(Schema.String),
3461 {
3462 schema: {
3463 "type": "array",
3464 "items": { "type": "string" }
3465 }
3466 }
3467 )
3468 assertJsonSchemaDocument(
3469 Schema.Array(Schema.String).annotate({ description: "a" }),
3470 {
3471 schema: {
3472 "type": "array",
3473 "items": { "type": "string" },
3474 "description": "a"
3475 }
3476 }
3477 )
3478 })
3479
3480 describe("checks", () => {
3481 it("isMinLength", () => {
3482 assertJsonSchemaDocument(
3483 Schema.Array(Schema.String).check(Schema.isMinLength(2)),
3484 {
3485 schema: {
3486 "type": "array",
3487 "items": { "type": "string" },
3488 "minItems": 2
3489 }
3490 }
3491 )
3492 })
3493
3494 it("isMaxLength", () => {
3495 assertJsonSchemaDocument(
3496 Schema.Array(Schema.String).check(Schema.isMaxLength(2)),
3497 {
3498 schema: {
3499 "type": "array",
3500 "items": { "type": "string" },
3501 "maxItems": 2
3502 }
3503 }
3504 )
3505 })
3506
3507 it("isBetweenLength", () => {
3508 assertJsonSchemaDocument(
3509 Schema.Array(Schema.String).check(Schema.isBetweenLength(2, 2)),
3510 {
3511 schema: {
3512 "type": "array",
3513 "items": { "type": "string" },
3514 "minItems": 2,
3515 "maxItems": 2
3516 }
3517 }
3518 )
3519 })
3520
3521 it("UniqueArray", () => {
3522 assertJsonSchemaDocument(
3523 Schema.UniqueArray(Schema.String),
3524 {
3525 schema: {
3526 "type": "array",
3527 "items": { "type": "string" },
3528 "uniqueItems": true
3529 }
3530 }
3531 )
3532 })
3533 })
3534 })
3535
3536 it("TupleWithRest", () => {
3537 assertJsonSchemaDocument(
3538 Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Finite]),
3539 {
3540 schema: {
3541 "type": "array",
3542 "prefixItems": [
3543 { "type": "string" }
3544 ],
3545 "items": { "type": "number" },
3546 "minItems": 1
3547 }
3548 }
3549 )
3550 })
3551
3552 describe("Union", () => {
3553 it("empty union", () => {
3554 const schema = Schema.Union([])
3555 assertJsonSchemaDocument(schema, {
3556 schema: {
3557 "not": {}
3558 }
3559 })
3560 assertJsonSchemaDocument(Schema.Union([]).annotate({ description: "a" }), {
3561 schema: {
3562 "not": {},
3563 "description": "a"
3564 }
3565 })
3566 })
3567
3568 it("single member", () => {
3569 const schema = Schema.Union([Schema.String])
3570 assertJsonSchemaDocument(schema, {
3571 schema: {
3572 "anyOf": [
3573 {
3574 "type": "string"
3575 }
3576 ]
3577 }
3578 })
3579 assertJsonSchemaDocument(Schema.Union([Schema.String]).annotate({ description: "a" }), {
3580 schema: {
3581 "anyOf": [
3582 {
3583 "type": "string"
3584 }
3585 ],
3586 "description": "a"
3587 }
3588 })
3589 assertJsonSchemaDocument(
3590 Schema.Union([Schema.String.annotate({ description: "inner" })]).annotate({ description: "outer" }),
3591 {
3592 schema: {
3593 "anyOf": [
3594 {
3595 "type": "string",
3596 "description": "inner"
3597 }
3598 ],
3599 "description": "outer"
3600 }
3601 }
3602 )
3603 })
3604
3605 it("uses anyOf when approximated oneOf branches can overlap", () => {
3606 const schema = Schema.Union([
3607 Schema.String.check(Schema.isMinLength(2)),
3608 Schema.String.check(Schema.isMaxLength(1))
3609 ], { mode: "oneOf" })
3610 assertJsonSchemaDocument(schema, {
3611 schema: {
3612 anyOf: [
3613 { type: "string", minLength: 1 },
3614 { type: "string", maxLength: 1 }
3615 ]
3616 }
3617 })
3618 assertJsonSchemaAcceptsEffectValues(schema, ["a", "ab", "😀"])
3619 })
3620
3621 it("keeps oneOf when its branches are exact", () => {
3622 assertJsonSchemaDocument(
3623 Schema.Union([Schema.String, Schema.Boolean], { mode: "oneOf" }),
3624 { schema: { oneOf: [{ type: "string" }, { type: "boolean" }] } }
3625 )
3626 })
3627
3628 it("does not compact duplicate enum values", () => {
3629 assertJsonSchemaDocument(
3630 Schema.Union([Schema.Literal("a"), Schema.Literal("a")]),
3631 {
3632 schema: {
3633 anyOf: [
3634 { type: "string", enum: ["a"] },
3635 { type: "string", enum: ["a"] }
3636 ]
3637 }
3638 }
3639 )
3640 })
3641
3642 it("String | Number", () => {
3643 assertJsonSchemaDocument(
3644 Schema.Union([
3645 Schema.String,
3646 Schema.Finite
3647 ]),
3648 {
3649 schema: {
3650 "anyOf": [
3651 { "type": "string" },
3652 { "type": "number" }
3653 ]
3654 }
3655 }
3656 )
3657 assertJsonSchemaDocument(
3658 Schema.Union([
3659 Schema.String,
3660 Schema.Finite
3661 ]).annotate({ description: "description" }),
3662 {
3663 schema: {
3664 "anyOf": [
3665 { "type": "string" },
3666 { "type": "number" }
3667 ],
3668 "description": "description"
3669 }
3670 }
3671 )
3672 })
3673
3674 it("String | BigInt", () => {
3675 assertJsonSchemaDocument(
3676 Schema.Union([
3677 Schema.String,
3678 Schema.BigInt
3679 ]),
3680 {
3681 schema: {
3682 "anyOf": [
3683 {
3684 "type": "string",
3685 "pattern": "^-?\\d+$"
3686 },
3687 { "type": "string" }
3688 ]
3689 }
3690 }
3691 )
3692 assertJsonSchemaDocument(
3693 Schema.Union([
3694 Schema.String,
3695 Schema.Finite
3696 ]).annotate({ description: "description" }),
3697 {
3698 schema: {
3699 "anyOf": [
3700 { "type": "string" },
3701 { "type": "number" }
3702 ],
3703 "description": "description"
3704 }
3705 }
3706 )
3707 })
3708 })
3709
3710 it("mutually recursive schemas", () => {
3711 interface Expression {
3712 readonly type: "expression"
3713 readonly value: number | Operation
3714 }
3715
3716 interface Operation {
3717 readonly type: "operation"
3718 readonly operator: "+" | "-"
3719 readonly left: Expression
3720 readonly right: Expression
3721 }
3722
3723 const Expression = Schema.Struct({
3724 type: Schema.Literal("expression"),
3725 value: Schema.Union([Schema.Finite, Schema.suspend((): Schema.Codec<Operation> => Operation)])
3726 }).annotate({ identifier: "Expression" })
3727
3728 const Operation = Schema.Struct({
3729 type: Schema.Literal("operation"),
3730 operator: Schema.Literals(["+", "-"]),
3731 left: Expression,
3732 right: Expression
3733 }).annotate({ identifier: "Operation" })
3734
3735 assertJsonSchemaDocument(
3736 Operation,
3737 {
3738 schema: {
3739 "$ref": "#/$defs/Operation"
3740 },
3741 definitions: {
3742 Operation: {
3743 "type": "object",
3744 "properties": {
3745 "type": {
3746 "type": "string",
3747 "enum": [
3748 "operation"
3749 ]
3750 },
3751 "operator": {
3752 "type": "string",
3753 "enum": [
3754 "+",
3755 "-"
3756 ]
3757 },
3758 "left": {
3759 "$ref": "#/$defs/Expression"
3760 },
3761 "right": {
3762 "$ref": "#/$defs/Expression"
3763 }
3764 },
3765 "required": [
3766 "type",
3767 "operator",
3768 "left",
3769 "right"
3770 ],
3771 "additionalProperties": false
3772 },
3773 Expression: {
3774 "type": "object",
3775 "properties": {
3776 "type": {
3777 "type": "string",
3778 "enum": [
3779 "expression"
3780 ]
3781 },
3782 "value": {
3783 "anyOf": [
3784 {
3785 "type": "number"
3786 },
3787 {
3788 "$ref": "#/$defs/Operation"
3789 }
3790 ]
3791 }
3792 },
3793 "required": [
3794 "type",
3795 "value"
3796 ],
3797 "additionalProperties": false
3798 }
3799 }
3800 }
3801 )
3802 assertJsonSchemaDocument(
3803 Expression,
3804 {
3805 schema: {
3806 "$ref": "#/$defs/Expression"
3807 },
3808 definitions: {
3809 Operation: {
3810 "type": "object",
3811 "properties": {
3812 "type": {
3813 "type": "string",
3814 "enum": [
3815 "operation"
3816 ]
3817 },
3818 "operator": {
3819 "type": "string",
3820 "enum": [
3821 "+",
3822 "-"
3823 ]
3824 },
3825 "left": {
3826 "$ref": "#/$defs/Expression"
3827 },
3828 "right": {
3829 "$ref": "#/$defs/Expression"
3830 }
3831 },
3832 "required": [
3833 "type",
3834 "operator",
3835 "left",
3836 "right"
3837 ],
3838 "additionalProperties": false
3839 },
3840 Expression: {
3841 "type": "object",
3842 "properties": {
3843 "type": {
3844 "type": "string",
3845 "enum": [
3846 "expression"
3847 ]
3848 },
3849 "value": {
3850 "anyOf": [
3851 {
3852 "type": "number"
3853 },
3854 {
3855 "$ref": "#/$defs/Operation"
3856 }
3857 ]
3858 }
3859 },
3860 "required": [
3861 "type",
3862 "value"
3863 ],
3864 "additionalProperties": false
3865 }
3866 }
3867 }
3868 )
3869 })
3870
3871 describe("fromJsonString", () => {
3872 it("top level fromJsonString", () => {
3873 assertJsonSchemaDocument(
3874 Schema.fromJsonString(Schema.FiniteFromString),
3875 {
3876 schema: {
3877 "type": "string",
3878 "contentMediaType": "application/json"
3879 }
3880 }
3881 )
3882 })
3883
3884 it("preserves the content schema identifier as a canonical reference", () => {
3885 const MyEvent = Schema.Struct({
3886 value: Schema.String
3887 }).annotate({ identifier: "MyEvent" })
3888
3889 assertJsonSchemaDocument(
3890 Schema.fromJsonString(MyEvent),
3891 {
3892 schema: {
3893 "$ref": "#/$defs/MyEventEncoded"
3894 },
3895 definitions: {
3896 "MyEventEncoded": {
3897 "type": "string",
3898 "contentMediaType": "application/json"
3899 }
3900 }
3901 }
3902 )
3903 })
3904
3905 it("respects an explicit encoded-side identifier", () => {
3906 const MyEvent = Schema.Struct({
3907 value: Schema.String
3908 }).annotate({ identifier: "MyEvent" })
3909 const MyWireEvent = Schema.flip(
3910 Schema.flip(Schema.fromJsonString(MyEvent)).annotate({ identifier: "MyWireEvent" })
3911 )
3912
3913 assertJsonSchemaDocument(
3914 MyWireEvent,
3915 {
3916 schema: {
3917 "$ref": "#/$defs/MyWireEvent"
3918 },
3919 definitions: {
3920 "MyWireEvent": {
3921 "type": "string",
3922 "contentMediaType": "application/json"
3923 }
3924 }
3925 }
3926 )
3927 })
3928 })
3929
3930 it("Class preserves its identifier as a canonical reference", () => {
3931 class A extends Schema.Class<A>("A")({
3932 a: Schema.String
3933 }) {}
3934 assertJsonSchemaDocument(
3935 A,
3936 {
3937 schema: {
3938 "$ref": "#/$defs/AEncoded"
3939 },
3940 definitions: {
3941 "AEncoded": {
3942 "type": "object",
3943 "properties": {
3944 "a": { "type": "string" }
3945 },
3946 "required": ["a"],
3947 "additionalProperties": false
3948 }
3949 }
3950 },
3951 { includeAnnotationKey: () => true }
3952 )
3953 })
3954
3955 it("Error preserves its identifier as a canonical reference", () => {
3956 class E extends Schema.Error<E>("E")({
3957 a: Schema.String
3958 }) {}
3959 assertJsonSchemaDocument(E, {
3960 schema: {
3961 "$ref": "#/$defs/EEncoded"
3962 },
3963 definitions: {
3964 "EEncoded": {
3965 "type": "object",
3966 "properties": {
3967 "a": { "type": "string" }
3968 },
3969 "required": ["a"],
3970 "additionalProperties": false
3971 }
3972 }
3973 })
3974 })
3975})