1import { assert } from "@effect/vitest"
2import { JsonSchema, Schema, SchemaRepresentation } from "effect"
3import { TestSchema } from "effect/testing"
4import { describe, it } from "vitest"
5import { deepStrictEqual, throws } from "../../utils/assert.ts"
6
7const makeCode = SchemaRepresentation.makeCode
8
9type Expected = {
10 readonly codes: SchemaRepresentation.Code
11 readonly references?: Partial<SchemaRepresentation.CodeDocument["references"]>
12}
13
14function toSchemaFromJsonSchemaDocument(
15 document: JsonSchema.Document<"draft-2020-12">,
16 options?: SchemaRepresentation.FromJsonSchemaOptions
17): Schema.Top {
18 return SchemaRepresentation.fromJsonSchemaDocument(document, { patterns: "apply", ...options })
19}
20
21describe("fromJsonSchemaDocument", () => {
22 it("round-trips optional Never properties (#8137)", () => {
23 const original = Schema.Struct({ value: Schema.optionalKey(Schema.Never) })
24 const imported = SchemaRepresentation.fromJsonSchemaDocument(Schema.toJsonSchemaDocument(original))
25 assertCode(imported, {
26 codes: makeCode(
27 `Schema.StructWithRest(Schema.Struct({ "value": Schema.optionalKey(Schema.Never) }), [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))])`,
28 `{ readonly "value"?: never } & { readonly [x: string]: Schema.Json }`
29 )
30 })
31 })
32
33 it("imports not-empty schemas at the root and through references", () => {
34 assertFromJsonSchema({ schema: { not: {}, description: "impossible" } }, {
35 codes: makeCode(`Schema.Never.annotate({ "description": "impossible" })`, `never`)
36 })
37 assertFromJsonSchema({
38 schema: {
39 type: "object",
40 properties: { value: { $ref: "#/$defs/Impossible" } },
41 required: ["value"],
42 $defs: { Impossible: { type: "string", not: {} } }
43 }
44 }, {
45 codes: makeCode(
46 `Schema.StructWithRest(Schema.Struct({ "value": Impossible }), [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))])`,
47 `{ readonly "value": Impossible } & { readonly [x: string]: Schema.Json }`
48 ),
49 references: {
50 nonRecursives: [{
51 $ref: `Impossible`,
52 code: makeCode(`Schema.Never.annotate({ "identifier": "Impossible" })`, `never`)
53 }]
54 }
55 })
56 })
57
58 it("preserves Never in array items and union members", () => {
59 assertFromJsonSchema(
60 { schema: { type: "array", items: { not: {} } } },
61 { codes: makeCode(`Schema.Array(Schema.Never)`, `ReadonlyArray<never>`) }
62 )
63 assertFromJsonSchema(
64 { schema: { type: "array", prefixItems: [{ not: {} }] } },
65 {
66 codes: makeCode(
67 `Schema.TupleWithRest(Schema.Tuple([Schema.optionalKey(Schema.Never)]), [Schema.Json.annotate({ "expected": "JSON value" })])`,
68 `readonly [(never)?, ...Array<Schema.Json>]`
69 )
70 }
71 )
72 assertFromJsonSchema(
73 { schema: { anyOf: [{ not: {} }, { type: "string" }] } },
74 { codes: makeCode(`Schema.Union([Schema.Never, Schema.String])`, `never | string`) }
75 )
76 assertFromJsonSchema(
77 { schema: { oneOf: [{ not: {} }, { type: "string" }] } },
78 { codes: makeCode(`Schema.Union([Schema.Never, Schema.String], { mode: "oneOf" })`, `never | string`) }
79 )
80 })
81
82 it("imports string lengths by code point and integers as safe integers", () => {
83 for (
84 const [source, input, expected] of [
85 [{ type: "string", minLength: 2 }, "😀", false],
86 [{ type: "string", maxLength: 1 }, "😀", true],
87 [{ type: "string", minLength: 1 }, "😀", true],
88 [{ type: "string", minLength: 1 }, "", false],
89 [{ type: "string", maxLength: 0 }, "", true],
90 [{ type: "string", maxLength: 0 }, "a", false],
91 [{ type: "string", minLength: 2 }, "é", false],
92 [{ type: "string", maxLength: 1 }, "é", true],
93 [{ type: "string", minLength: 2 }, "e\u0301", true],
94 [{ type: "string", maxLength: 1 }, "e\u0301", false],
95 [{ type: "string", minLength: 2 }, "\uD800", false],
96 [{ type: "string", maxLength: 1 }, "\uD800", true],
97 [{ type: "integer" }, 1e20, false],
98 [{ type: "string", pattern: "^.$" }, "😀", true]
99 ] as const
100 ) {
101 const schema = toSchemaFromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12(source))
102 assert.strictEqual(Schema.is(schema)(input), expected)
103 }
104 })
105
106 it("uses imported string semantics while intersecting primitive literals", () => {
107 const minLength = toSchemaFromJsonSchemaDocument(
108 JsonSchema.fromSchemaDraft2020_12({ enum: ["😀", "e\u0301"], minLength: 2 })
109 )
110 assert.isFalse(Schema.is(minLength)("😀"))
111 assert.isTrue(Schema.is(minLength)("e\u0301"))
112
113 const pattern = toSchemaFromJsonSchemaDocument(
114 JsonSchema.fromSchemaDraft2020_12({ const: "😀", pattern: "^.$" })
115 )
116 assert.isTrue(Schema.is(pattern)("😀"))
117 })
118
119 it("reports patterns unsupported in ECMAScript Unicode mode", () => {
120 throws(
121 () =>
122 toSchemaFromJsonSchemaDocument(
123 JsonSchema.fromSchemaDraft2020_12({ type: "string", pattern: "\\a" })
124 ),
125 `Cannot import pattern using ECMAScript Unicode mode.\n at ["schema"]["pattern"]`
126 )
127 })
128
129 function assertFromJsonSchema(
130 input: {
131 readonly schema: JsonSchema.JsonSchema
132 readonly options?: SchemaRepresentation.FromJsonSchemaOptions
133 },
134 expected: Expected
135 ) {
136 const jsonDocument = JsonSchema.fromSchemaDraft2020_12(input.schema)
137 const schema = toSchemaFromJsonSchemaDocument(jsonDocument, input.options)
138 assertCode(schema, expected)
139 }
140
141 function assertCode(schema: Schema.Top, expected: Expected) {
142 deepStrictEqual(SchemaRepresentation.toCodeDocument(SchemaRepresentation.toRepresentations([schema.ast])), {
143 codes: [expected.codes],
144 references: {
145 nonRecursives: expected.references?.nonRecursives ?? [],
146 recursives: expected.references?.recursives ?? {}
147 },
148 artifacts: []
149 })
150 }
151
152 it("unconstrained schema", () => {
153 assertFromJsonSchema(
154 { schema: {} },
155 { codes: makeCode(`Schema.Json.annotate({ "expected": "JSON value" })`, `Schema.Json`) }
156 )
157 assertFromJsonSchema(
158 {
159 schema: {
160 title: "a",
161 description: "b",
162 default: "c",
163 examples: ["d"],
164 readOnly: true,
165 writeOnly: true
166 }
167 },
168 {
169 codes: makeCode(
170 `Schema.Json.annotate({ "expected": "JSON value", "title": "a", "description": "b", "default": "c", "examples": ["d"], "readOnly": true, "writeOnly": true })`,
171 `Schema.Json`
172 )
173 }
174 )
175 })
176
177 it("keeps annotation-only schemas unconstrained", () => {
178 assertFromJsonSchema({ schema: { format: "email" } }, {
179 codes: makeCode(`Schema.Json.annotate({ "expected": "JSON value", "format": "email" })`, `Schema.Json`)
180 })
181 })
182
183 describe("const", () => {
184 it("string literal", () => {
185 assertFromJsonSchema(
186 { schema: { const: "a" } },
187 { codes: makeCode(`Schema.Literal("a")`, `"a"`) }
188 )
189 assertFromJsonSchema(
190 { schema: { const: "a", description: "a" } },
191 { codes: makeCode(`Schema.Literal("a").annotate({ "description": "a" })`, `"a"`) }
192 )
193 })
194
195 it("const: literal (number)", () => {
196 assertFromJsonSchema(
197 { schema: { const: 1 } },
198 { codes: makeCode(`Schema.Literal(1)`, `1`) }
199 )
200 })
201
202 it("const: literal (boolean)", () => {
203 assertFromJsonSchema(
204 { schema: { const: true } },
205 { codes: makeCode(`Schema.Literal(true)`, `true`) }
206 )
207 })
208
209 it("null literal", () => {
210 assertFromJsonSchema(
211 { schema: { const: null } },
212 { codes: makeCode(`Schema.Null`, `null`) }
213 )
214 assertFromJsonSchema(
215 { schema: { const: null, description: "a" } },
216 { codes: makeCode(`Schema.Null.annotate({ "description": "a" })`, `null`) }
217 )
218 })
219
220 it("rejects structured values", () => {
221 for (const value of [{}, []]) {
222 throws(
223 () => toSchemaFromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12({ const: value })),
224 `Only primitive values are supported in "const" and "enum".\n at ["schema"]["const"]`
225 )
226 }
227 })
228 })
229
230 describe("enum", () => {
231 it("single string member", () => {
232 assertFromJsonSchema(
233 { schema: { enum: ["a"] } },
234 { codes: makeCode(`Schema.Literal("a")`, `"a"`) }
235 )
236 assertFromJsonSchema(
237 { schema: { enum: ["a"], description: "a" } },
238 { codes: makeCode(`Schema.Literal("a").annotate({ "description": "a" })`, `"a"`) }
239 )
240 })
241
242 it("single enum (number)", () => {
243 assertFromJsonSchema(
244 { schema: { enum: [1] } },
245 { codes: makeCode(`Schema.Literal(1)`, `1`) }
246 )
247 })
248
249 it("single enum (boolean)", () => {
250 assertFromJsonSchema(
251 { schema: { enum: [true] } },
252 { codes: makeCode(`Schema.Literal(true)`, `true`) }
253 )
254 })
255
256 it("multiple literal members", () => {
257 assertFromJsonSchema(
258 { schema: { enum: ["a", 1] } },
259 { codes: makeCode(`Schema.Literals(["a", 1])`, `"a" | 1`) }
260 )
261 assertFromJsonSchema(
262 { schema: { enum: ["a", 1], description: "a" } },
263 { codes: makeCode(`Schema.Literals(["a", 1]).annotate({ "description": "a" })`, `"a" | 1`) }
264 )
265 })
266
267 it("enum containing null", () => {
268 assertFromJsonSchema(
269 { schema: { enum: ["a", null] } },
270 { codes: makeCode(`Schema.Union([Schema.Literal("a"), Schema.Null])`, `"a" | null`) }
271 )
272 })
273 })
274
275 it("anyOf", () => {
276 assertFromJsonSchema(
277 { schema: { anyOf: [{ const: "a" }, { enum: [1, 2] }] } },
278 { codes: makeCode(`Schema.Union([Schema.Literal("a"), Schema.Literals([1, 2])])`, `"a" | 1 | 2`) }
279 )
280 })
281
282 it("anyOf with siblings", () => {
283 assertFromJsonSchema(
284 {
285 schema: {
286 type: "object",
287 properties: { id: { type: "string" } },
288 required: ["id"],
289 anyOf: [
290 { properties: { a: { type: "string" } }, required: ["a"] },
291 { properties: { b: { type: "number" } }, required: ["b"] }
292 ]
293 }
294 },
295 {
296 codes: makeCode(
297 `Schema.Union([Schema.StructWithRest(Schema.Struct({ "a": Schema.String, "id": Schema.String }), [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))]), Schema.StructWithRest(Schema.Struct({ "b": Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), "id": Schema.String }), [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))])])`,
298 `{ readonly "a": string, readonly "id": string } & { readonly [x: string]: Schema.Json } | { readonly "b": number, readonly "id": string } & { readonly [x: string]: Schema.Json }`
299 )
300 }
301 )
302 })
303
304 it("oneOf", () => {
305 assertFromJsonSchema(
306 { schema: { oneOf: [{ const: "a" }, { enum: [1, 2] }] } },
307 {
308 codes: makeCode(
309 `Schema.Union([Schema.Literal("a"), Schema.Literals([1, 2])], { mode: "oneOf" })`,
310 `"a" | 1 | 2`
311 )
312 }
313 )
314 })
315
316 it("oneOf with siblings", () => {
317 assertFromJsonSchema(
318 {
319 schema: {
320 type: "object",
321 properties: { id: { type: "string" } },
322 required: ["id"],
323 oneOf: [
324 { properties: { a: { type: "string" } }, required: ["a"] },
325 { properties: { b: { type: "number" } }, required: ["b"] }
326 ]
327 }
328 },
329 {
330 codes: makeCode(
331 `Schema.Union([Schema.StructWithRest(Schema.Struct({ "a": Schema.String, "id": Schema.String }), [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))]), Schema.StructWithRest(Schema.Struct({ "b": Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), "id": Schema.String }), [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))])], { mode: "oneOf" })`,
332 `{ readonly "a": string, readonly "id": string } & { readonly [x: string]: Schema.Json } | { readonly "b": number, readonly "id": string } & { readonly [x: string]: Schema.Json }`
333 )
334 }
335 )
336 })
337
338 describe("type: null", () => {
339 it("type only", () => {
340 assertFromJsonSchema(
341 { schema: { type: "null" } },
342 { codes: makeCode(`Schema.Null`, `null`) }
343 )
344 })
345 })
346
347 describe("type: string", () => {
348 it("type only", () => {
349 assertFromJsonSchema(
350 { schema: { type: "string" } },
351 { codes: makeCode(`Schema.String`, `string`) }
352 )
353 })
354
355 describe("checks", () => {
356 it("minLength", () => {
357 assertFromJsonSchema(
358 { schema: { type: "string", minLength: 1 } },
359 {
360 codes: makeCode(
361 `Schema.String.check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }))`,
362 `string`
363 )
364 }
365 )
366 })
367
368 it("maxLength", () => {
369 assertFromJsonSchema(
370 { schema: { type: "string", maxLength: 1 } },
371 {
372 codes: makeCode(
373 `Schema.String.check(Schema.isMaxCodePoints(1).annotate({ "expected": "a string with at most 1 code points" }))`,
374 `string`
375 )
376 }
377 )
378 })
379
380 it("pattern with an explicit string type", () => {
381 assertFromJsonSchema(
382 { schema: { type: "string", pattern: "a*" } },
383 {
384 codes: makeCode(
385 `Schema.String.check(Schema.isPattern(new RegExp("a*", "u")).annotate({ "expected": "a string matching the RegExp a*" }))`,
386 `string`
387 )
388 }
389 )
390 })
391
392 it("pattern only constrains strings", () => {
393 assertFromJsonSchema({ schema: { pattern: "^a+$" } }, {
394 codes: makeCode(
395 `Schema.Union([Schema.Null, Schema.String.check(Schema.isPattern(new RegExp("^a+$", "u")).annotate({ "expected": "a string matching the RegExp ^a+$" })), Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Boolean, Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Array(Schema.Json.annotate({ "expected": "JSON value" }))])`,
396 `null | string | number | boolean | { readonly [x: string]: Schema.Json } | ReadonlyArray<Schema.Json>`
397 )
398 })
399 })
400 })
401 })
402
403 describe("type: number", () => {
404 it("type only", () => {
405 assertFromJsonSchema(
406 { schema: { type: "number" } },
407 {
408 codes: makeCode(
409 `Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" }))`,
410 `number`
411 )
412 }
413 )
414 })
415
416 describe("checks", () => {
417 it("minimum", () => {
418 assertFromJsonSchema(
419 { schema: { type: "number", minimum: 1 } },
420 {
421 codes: makeCode(
422 `Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })).check(Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" }))`,
423 `number`
424 )
425 }
426 )
427 })
428
429 it("maximum", () => {
430 assertFromJsonSchema(
431 { schema: { type: "number", maximum: 1 } },
432 {
433 codes: makeCode(
434 `Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })).check(Schema.isLessThanOrEqualTo(1).annotate({ "expected": "a value less than or equal to 1" }))`,
435 `number`
436 )
437 }
438 )
439 })
440
441 it("exclusiveMinimum", () => {
442 assertFromJsonSchema(
443 { schema: { type: "number", exclusiveMinimum: 1 } },
444 {
445 codes: makeCode(
446 `Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })).check(Schema.isGreaterThan(1).annotate({ "expected": "a value greater than 1" }))`,
447 `number`
448 )
449 }
450 )
451 })
452
453 it("exclusiveMaximum", () => {
454 assertFromJsonSchema(
455 { schema: { type: "number", exclusiveMaximum: 1 } },
456 {
457 codes: makeCode(
458 `Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })).check(Schema.isLessThan(1).annotate({ "expected": "a value less than 1" }))`,
459 `number`
460 )
461 }
462 )
463 })
464
465 it("multipleOf", () => {
466 assertFromJsonSchema(
467 { schema: { type: "number", multipleOf: 1 } },
468 {
469 codes: makeCode(
470 `Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })).check(Schema.isMultipleOf(1).annotate({ "expected": "a value that is a multiple of 1" }))`,
471 `number`
472 )
473 }
474 )
475 })
476 })
477 })
478
479 describe("type: integer", () => {
480 it("type only", () => {
481 assertFromJsonSchema(
482 { schema: { type: "integer" } },
483 { codes: makeCode(`Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))`, `number`) }
484 )
485 })
486
487 describe("checks", () => {
488 it("minimum", () => {
489 assertFromJsonSchema(
490 { schema: { type: "integer", minimum: 1 } },
491 {
492 codes: makeCode(
493 `Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check(Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" }))`,
494 `number`
495 )
496 }
497 )
498 })
499
500 it("maximum", () => {
501 assertFromJsonSchema(
502 { schema: { type: "integer", maximum: 1 } },
503 {
504 codes: makeCode(
505 `Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check(Schema.isLessThanOrEqualTo(1).annotate({ "expected": "a value less than or equal to 1" }))`,
506 `number`
507 )
508 }
509 )
510 })
511
512 it("exclusiveMinimum", () => {
513 assertFromJsonSchema(
514 { schema: { type: "integer", exclusiveMinimum: 1 } },
515 {
516 codes: makeCode(
517 `Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check(Schema.isGreaterThan(1).annotate({ "expected": "a value greater than 1" }))`,
518 `number`
519 )
520 }
521 )
522 })
523
524 it("exclusiveMaximum", () => {
525 assertFromJsonSchema(
526 { schema: { type: "integer", exclusiveMaximum: 1 } },
527 {
528 codes: makeCode(
529 `Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check(Schema.isLessThan(1).annotate({ "expected": "a value less than 1" }))`,
530 `number`
531 )
532 }
533 )
534 })
535
536 it("multipleOf", () => {
537 assertFromJsonSchema(
538 { schema: { type: "integer", multipleOf: 1 } },
539 {
540 codes: makeCode(
541 `Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check(Schema.isMultipleOf(1).annotate({ "expected": "a value that is a multiple of 1" }))`,
542 `number`
543 )
544 }
545 )
546 })
547 })
548 })
549
550 describe("type: boolean", () => {
551 it("type only", () => {
552 assertFromJsonSchema(
553 { schema: { type: "boolean" } },
554 { codes: makeCode(`Schema.Boolean`, `boolean`) }
555 )
556 })
557 })
558
559 describe("type: array", () => {
560 it("type only", () => {
561 assertFromJsonSchema(
562 { schema: { type: "array" } },
563 {
564 codes: makeCode(
565 `Schema.Array(Schema.Json.annotate({ "expected": "JSON value" }))`,
566 `ReadonlyArray<Schema.Json>`
567 )
568 }
569 )
570 })
571
572 it("items", () => {
573 assertFromJsonSchema(
574 {
575 schema: {
576 type: "array",
577 items: { type: "string" }
578 }
579 },
580 { codes: makeCode(`Schema.Array(Schema.String)`, `ReadonlyArray<string>`) }
581 )
582 })
583
584 it("prefixItems preserves maxItems below the prefix length", () => {
585 assertFromJsonSchema(
586 {
587 schema: {
588 type: "array",
589 prefixItems: [{ type: "string" }, { type: "number" }],
590 maxItems: 1
591 }
592 },
593 {
594 codes: makeCode(
595 `Schema.TupleWithRest(Schema.Tuple([Schema.optionalKey(Schema.String), Schema.optionalKey(Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })))]), [Schema.Json.annotate({ "expected": "JSON value" })]).check(Schema.isMaxLength(1).annotate({ "expected": "a value with a length of at most 1" }))`,
596 `readonly [(string)?, (number)?, ...Array<Schema.Json>]`
597 )
598 }
599 )
600 })
601
602 it("prefixItems preserves maxItems above the prefix length", () => {
603 assertFromJsonSchema(
604 {
605 schema: {
606 type: "array",
607 prefixItems: [{ type: "string" }],
608 maxItems: 2
609 }
610 },
611 {
612 codes: makeCode(
613 `Schema.TupleWithRest(Schema.Tuple([Schema.optionalKey(Schema.String)]), [Schema.Json.annotate({ "expected": "JSON value" })]).check(Schema.isMaxLength(2).annotate({ "expected": "a value with a length of at most 2" }))`,
614 `readonly [(string)?, ...Array<Schema.Json>]`
615 )
616 }
617 )
618 })
619
620 it("prefixItems omits maxItems redundant with items: false", () => {
621 assertFromJsonSchema(
622 {
623 schema: {
624 type: "array",
625 prefixItems: [{ type: "string" }],
626 items: false,
627 maxItems: 2
628 }
629 },
630 { codes: makeCode(`Schema.Tuple([Schema.optionalKey(Schema.String)])`, `readonly [(string)?]`) }
631 )
632 })
633
634 it("prefixItems closes when maxItems equals the prefix length", () => {
635 assertFromJsonSchema(
636 {
637 schema: {
638 type: "array",
639 prefixItems: [{ type: "string" }],
640 maxItems: 1
641 }
642 },
643 { codes: makeCode(`Schema.Tuple([Schema.optionalKey(Schema.String)])`, `readonly [(string)?]`) }
644 )
645 })
646
647 it("prefixItems marks elements required by minItems", () => {
648 assertFromJsonSchema(
649 {
650 schema: {
651 type: "array",
652 prefixItems: [{ type: "string" }],
653 minItems: 1,
654 maxItems: 1
655 }
656 },
657 { codes: makeCode(`Schema.Tuple([Schema.String])`, `readonly [string]`) }
658 )
659 })
660
661 it("prefixItems & minItems", () => {
662 assertFromJsonSchema(
663 {
664 schema: {
665 type: "array",
666 prefixItems: [{ type: "string" }],
667 minItems: 1,
668 items: { type: "number" }
669 }
670 },
671 {
672 codes: makeCode(
673 `Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" }))])`,
674 `readonly [string, ...Array<number>]`
675 )
676 }
677 )
678 })
679
680 describe("checks", () => {
681 it("minItems", () => {
682 assertFromJsonSchema(
683 { schema: { type: "array", minItems: 1 } },
684 {
685 codes: makeCode(
686 `Schema.Array(Schema.Json.annotate({ "expected": "JSON value" })).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }))`,
687 `ReadonlyArray<Schema.Json>`
688 )
689 }
690 )
691 })
692
693 it("maxItems", () => {
694 assertFromJsonSchema(
695 { schema: { type: "array", maxItems: 1 } },
696 {
697 codes: makeCode(
698 `Schema.Array(Schema.Json.annotate({ "expected": "JSON value" })).check(Schema.isMaxLength(1).annotate({ "expected": "a value with a length of at most 1" }))`,
699 `ReadonlyArray<Schema.Json>`
700 )
701 }
702 )
703 })
704
705 it("uniqueItems", () => {
706 assertFromJsonSchema(
707 { schema: { type: "array", uniqueItems: true } },
708 {
709 codes: makeCode(
710 `Schema.Array(Schema.Json.annotate({ "expected": "JSON value" })).check(Schema.isUnique().annotate({ "expected": "an array with unique items" }))`,
711 `ReadonlyArray<Schema.Json>`
712 )
713 }
714 )
715 })
716
717 it("does not require uniqueness when uniqueItems is false", () => {
718 assertFromJsonSchema({ schema: { type: "array", uniqueItems: false } }, {
719 codes: makeCode(
720 `Schema.Array(Schema.Json.annotate({ "expected": "JSON value" }))`,
721 `ReadonlyArray<Schema.Json>`
722 )
723 })
724 })
725 })
726 })
727
728 describe("type: object", () => {
729 it("delegates excess properties of closed structs to ParseOptions", async () => {
730 const schema = toSchemaFromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12({
731 type: "object",
732 properties: { name: { type: "string" } },
733 additionalProperties: false
734 }))
735 const asserts = new TestSchema.Asserts(schema as unknown as Schema.ConstraintDecoder<unknown>)
736 const decoding = asserts.decoding()
737 const strictDecoding = asserts.decoding({ parseOptions: { onExcessProperty: "error" } })
738 const input = { extra: 1 }
739 await decoding.succeed(input, {})
740 assert.deepStrictEqual(input, { extra: 1 })
741 await strictDecoding.fail(input, `Expected no excess property\n at ["extra"]`)
742 await strictDecoding.succeed({})
743 for (const value of [null, false, 0, "", [], undefined]) {
744 await decoding.fail(value, "Expected object")
745 }
746 })
747
748 it("preserves JSON-valued additional properties for every open form", async () => {
749 for (const additionalProperties of [undefined, true, {}]) {
750 const schema = toSchemaFromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12({
751 type: "object",
752 properties: { name: { type: "string" } },
753 ...(additionalProperties === undefined ? {} : { additionalProperties })
754 }))
755 const asserts = new TestSchema.Asserts(schema as unknown as Schema.ConstraintDecoder<unknown>)
756 const decoding = asserts.decoding()
757 const input = { name: "Mario", extra: { values: [1, null, true] } }
758 await decoding.succeed(input)
759 await asserts.decoding({ parseOptions: { onExcessProperty: "error" } }).succeed(input)
760 for (const extra of [undefined, Infinity, Symbol("extra"), () => 1]) {
761 await decoding.fail({ extra }, `Expected JSON value\n at ["extra"]`)
762 }
763 }
764 })
765
766 it("runs property checks after stripping excess properties", async () => {
767 const schema = toSchemaFromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12({
768 type: "object",
769 properties: { name: { type: "string" } },
770 additionalProperties: false,
771 maxProperties: 1,
772 propertyNames: { enum: ["name"] }
773 }))
774 const asserts = new TestSchema.Asserts(schema as unknown as Schema.ConstraintDecoder<unknown>)
775 await asserts.decoding().succeed({ name: "Mario", extra: 1 }, { name: "Mario" })
776 await asserts.decoding({ parseOptions: { onExcessProperty: "error" } }).fail(
777 { name: "Mario", extra: 1 },
778 `Expected no excess property\n at ["extra"]`
779 )
780 })
781
782 it("allows additional properties by default", () => {
783 assertFromJsonSchema(
784 { schema: { type: "object" } },
785 {
786 codes: makeCode(
787 `Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))`,
788 `{ readonly [x: string]: Schema.Json }`
789 )
790 }
791 )
792 })
793
794 it("closes an object when additionalProperties is false", () => {
795 assertFromJsonSchema(
796 {
797 schema: {
798 type: "object",
799 additionalProperties: false
800 }
801 },
802 { codes: makeCode(`Schema.Record(Schema.String, Schema.Never)`, `{ readonly [x: string]: never }`) }
803 )
804 })
805
806 it("preserves closed empty objects through representation persistence and code generation", async () => {
807 const schema = toSchemaFromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12({
808 type: "object",
809 additionalProperties: false
810 }))
811 const persisted = SchemaRepresentation.toJson(SchemaRepresentation.toRepresentation(schema.ast))
812 const revived = SchemaRepresentation.fromRepresentation(SchemaRepresentation.fromJson(persisted), {
813 revivers: []
814 })
815 for (const current of [schema, revived]) {
816 const asserts = new TestSchema.Asserts(current as unknown as Schema.ConstraintDecoder<unknown>)
817 for (
818 const decoding of [asserts.decoding(), asserts.decoding({ parseOptions: { onExcessProperty: "error" } })]
819 ) {
820 await decoding.succeed({})
821 for (const value of [1, undefined]) {
822 await decoding.fail({ extra: value }, `Expected never\n at ["extra"]`)
823 }
824 await decoding.fail([], "Expected object")
825 }
826 }
827 assertCode(revived, {
828 codes: makeCode(`Schema.Record(Schema.String, Schema.Never)`, `{ readonly [x: string]: never }`)
829 })
830 })
831
832 it("additionalProperties", () => {
833 assertFromJsonSchema(
834 {
835 schema: {
836 type: "object",
837 additionalProperties: { type: "boolean" }
838 }
839 },
840 { codes: makeCode(`Schema.Record(Schema.String, Schema.Boolean)`, `{ readonly [x: string]: boolean }`) }
841 )
842 })
843
844 it("properties", () => {
845 assertFromJsonSchema(
846 {
847 schema: {
848 type: "object",
849 properties: { a: { type: "string" }, b: { type: "string" } },
850 required: ["a"],
851 additionalProperties: false
852 }
853 },
854 {
855 codes: makeCode(
856 `Schema.Struct({ "a": Schema.String, "b": Schema.optionalKey(Schema.String) })`,
857 `{ readonly "a": string, readonly "b"?: string }`
858 )
859 }
860 )
861 })
862
863 it("properties & additionalProperties", () => {
864 throws(
865 () =>
866 toSchemaFromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12({
867 type: "object",
868 properties: { a: { type: "string" } },
869 required: ["a"],
870 additionalProperties: { type: "boolean" }
871 })),
872 `Cannot combine typed "additionalProperties" with other property schemas: Effect index signatures also check excluded keys.\n at ["schema"]`
873 )
874 })
875
876 it("explains why typed additional properties cannot exclude patterned keys", () => {
877 throws(
878 () =>
879 toSchemaFromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12({
880 type: "object",
881 patternProperties: { "^a": { type: "string" } },
882 additionalProperties: { type: "number" }
883 })),
884 `Cannot combine typed "additionalProperties" with other property schemas: Effect index signatures also check excluded keys.\n at ["schema"]`
885 )
886 })
887
888 it("explains the supported closed pattern form when properties are declared or required", () => {
889 for (const fields of [{ properties: { a: { type: "number" } } }, { required: ["a"] }] as const) {
890 throws(
891 () =>
892 toSchemaFromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12({
893 type: "object",
894 patternProperties: { "^a": { type: "number" } },
895 additionalProperties: false,
896 ...fields
897 })),
898 `Cannot import this closed patterned object: only one pattern without properties is supported.\n at ["schema"]`
899 )
900 }
901 })
902
903 it("round-trips a closed Record with a patterned key", async () => {
904 const original = Schema.Record(Schema.String.check(Schema.isStartingWith("a")), Schema.Finite)
905 const document = Schema.toJsonSchemaDocument(original, { onExcessProperty: "error" })
906 assert.deepStrictEqual(document.schema, {
907 type: "object",
908 patternProperties: { "^a": { type: "number" } },
909 additionalProperties: false
910 })
911 const imported = toSchemaFromJsonSchemaDocument(document)
912 const asserts = new TestSchema.Asserts(imported as unknown as Schema.ConstraintDecoder<unknown>)
913 const decoding = asserts.decoding()
914 const strictDecoding = asserts.decoding({ parseOptions: { onExcessProperty: "error" } })
915 for (const input of [{}, { a: 1 }, { a: 1, abc: 2 }]) {
916 await decoding.succeed(input)
917 await strictDecoding.succeed(input)
918 }
919 const input = { a: 1, z: "extra" }
920 await decoding.succeed(input, { a: 1 })
921 assert.deepStrictEqual(input, { a: 1, z: "extra" })
922 await decoding.succeed({ z: 1 }, {})
923 await strictDecoding.fail(input, `Expected no excess property\n at ["z"]`)
924 await strictDecoding.fail({ z: 1 }, `Expected no excess property\n at ["z"]`)
925 for (const value of ["x", null, undefined]) {
926 await decoding.fail({ a: value }, `Expected number\n at ["a"]`)
927 await strictDecoding.fail({ a: value }, `Expected number\n at ["a"]`)
928 }
929 await decoding.fail({ a: Infinity }, `Expected a finite number\n at ["a"]`)
930 await strictDecoding.fail({ a: Infinity }, `Expected a finite number\n at ["a"]`)
931 for (const value of [null, false, 0, "", [], undefined]) {
932 await decoding.fail(value, "Expected object")
933 }
934 assert.deepStrictEqual(Schema.toJsonSchemaDocument(imported, { onExcessProperty: "error" }), document)
935 })
936
937 it("imports a closed patterned Record through references", () => {
938 assertFromJsonSchema({
939 schema: {
940 type: "object",
941 properties: { values: { $ref: "#/$defs/Values" } },
942 required: ["values"],
943 additionalProperties: false,
944 $defs: {
945 Values: {
946 type: "object",
947 patternProperties: { a: { $ref: "#/$defs/Value" } },
948 additionalProperties: false
949 },
950 Value: { type: "string", minLength: 2 }
951 }
952 }
953 }, {
954 codes: makeCode(`Schema.Struct({ "values": Values })`, `{ readonly "values": Values }`),
955 references: {
956 nonRecursives: [{
957 $ref: `Value`,
958 code: makeCode(
959 `Schema.String.check(Schema.isMinCodePoints(2).annotate({ "expected": "a string with at least 2 code points", "identifier": "Value" }))`,
960 `string`
961 )
962 }, {
963 $ref: `Values`,
964 code: makeCode(
965 `Schema.Record(Schema.String.check(Schema.isPattern(new RegExp("a", "u")).annotate({ "expected": "a string matching the RegExp a" })), Value).annotate({ "identifier": "Values" })`,
966 `{ readonly [x: string]: Value }`
967 )
968 }]
969 }
970 })
971 })
972
973 it("keeps a closed pattern with Never values as an object schema", () => {
974 assertFromJsonSchema({
975 schema: {
976 type: "object",
977 patternProperties: { "^a": { not: {} } },
978 additionalProperties: false
979 }
980 }, {
981 codes: makeCode(
982 `Schema.Record(Schema.String.check(Schema.isPattern(new RegExp("^a", "u")).annotate({ "expected": "a string matching the RegExp ^a" })), Schema.Never)`,
983 `{ readonly [x: string]: never }`
984 )
985 })
986 })
987
988 it("matches finite patternProperties keys in Unicode mode", () => {
989 const document = JsonSchema.fromSchemaDraft2020_12({
990 allOf: [
991 {
992 type: "object",
993 properties: { "😀": {} },
994 additionalProperties: false
995 },
996 {
997 type: "object",
998 patternProperties: { "^.$": { type: "string" } }
999 }
1000 ]
1001 })
1002 const schema = toSchemaFromJsonSchemaDocument(document)
1003 assert.isTrue(Schema.is(schema)({ "😀": "value" }))
1004 assert.isFalse(Schema.is(schema)({ "😀": 1 }))
1005 assertCode(schema, {
1006 codes: makeCode(`Schema.Struct({ "😀": Schema.optionalKey(Schema.String) })`, `{ readonly "😀"?: string }`)
1007 })
1008 })
1009
1010 it("rejects closed multiple pattern properties", () => {
1011 throws(
1012 () =>
1013 toSchemaFromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12({
1014 type: "object",
1015 patternProperties: {
1016 "a*": { type: "string" },
1017 "b*": { type: "number" }
1018 },
1019 additionalProperties: false
1020 })),
1021 `Cannot import this closed patterned object: only one pattern without properties is supported.\n at ["schema"]`
1022 )
1023 })
1024
1025 describe("checks", () => {
1026 it("minProperties", () => {
1027 assertFromJsonSchema(
1028 { schema: { type: "object", minProperties: 1 } },
1029 {
1030 codes: makeCode(
1031 `Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })).check(Schema.isMinProperties(1).annotate({ "expected": "a value with at least 1 entry" }))`,
1032 `{ readonly [x: string]: Schema.Json }`
1033 )
1034 }
1035 )
1036 })
1037
1038 it("maxProperties", () => {
1039 assertFromJsonSchema(
1040 { schema: { type: "object", maxProperties: 1 } },
1041 {
1042 codes: makeCode(
1043 `Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })).check(Schema.isMaxProperties(1).annotate({ "expected": "a value with at most 1 entry" }))`,
1044 `{ readonly [x: string]: Schema.Json }`
1045 )
1046 }
1047 )
1048 })
1049 })
1050
1051 describe("propertyNames", () => {
1052 it("pattern", () => {
1053 assertFromJsonSchema(
1054 {
1055 schema: {
1056 type: "object",
1057 propertyNames: { pattern: "^[A-Z]" }
1058 }
1059 },
1060 {
1061 codes: makeCode(
1062 `Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })).check(Schema.isPropertyNames(Schema.String.check(Schema.isPattern(new RegExp("^[A-Z]", "u")).annotate({ "expected": "a string matching the RegExp ^[A-Z]" }))).annotate({ "expected": "an object with property names matching the schema" }))`,
1063 `{ readonly [x: string]: Schema.Json }`
1064 )
1065 }
1066 )
1067 })
1068
1069 it("false", () => {
1070 assertFromJsonSchema(
1071 {
1072 schema: {
1073 type: "object",
1074 propertyNames: false
1075 }
1076 },
1077 {
1078 codes: makeCode(
1079 `Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })).check(Schema.isPropertyNames(Schema.Never).annotate({ "expected": "an object with property names matching the schema" }))`,
1080 `{ readonly [x: string]: Schema.Json }`
1081 )
1082 }
1083 )
1084 })
1085
1086 it("allOf combines checks", () => {
1087 assertFromJsonSchema(
1088 {
1089 schema: {
1090 allOf: [
1091 { type: "object", propertyNames: { pattern: "^[A-Z]" } },
1092 { type: "object", propertyNames: { minLength: 2 } }
1093 ]
1094 }
1095 },
1096 {
1097 codes: makeCode(
1098 `Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })).check(Schema.isPropertyNames(Schema.String.check(Schema.isPattern(new RegExp("^[A-Z]", "u")).annotate({ "expected": "a string matching the RegExp ^[A-Z]" }))).annotate({ "expected": "an object with property names matching the schema" })).check(Schema.isPropertyNames(Schema.String.check(Schema.isMinCodePoints(2).annotate({ "expected": "a string with at least 2 code points" }))).annotate({ "expected": "an object with property names matching the schema" }))`,
1099 `{ readonly [x: string]: Schema.Json }`
1100 )
1101 }
1102 )
1103 })
1104 })
1105 })
1106
1107 it("array of types", () => {
1108 assertFromJsonSchema(
1109 {
1110 schema: { type: ["string", "null"] }
1111 },
1112 { codes: makeCode(`Schema.Union([Schema.String, Schema.Null])`, `string | null`) }
1113 )
1114 assertFromJsonSchema(
1115 {
1116 schema: {
1117 type: ["string", "null"],
1118 description: "a"
1119 }
1120 },
1121 {
1122 codes: makeCode(`Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "a" })`, `string | null`)
1123 }
1124 )
1125 })
1126
1127 it("ignores true schemas in allOf", () => {
1128 assertFromJsonSchema(
1129 { schema: { allOf: [true, { type: "string" }] } },
1130 { codes: makeCode(`Schema.String`, `string`) }
1131 )
1132 })
1133
1134 it("rejects structured enum members", () => {
1135 for (const value of [[], {}, { not: "data" }]) {
1136 throws(
1137 () => toSchemaFromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12({ enum: ["a", value] })),
1138 `Only primitive values are supported in "const" and "enum".\n at ["schema"]["enum"][1]`
1139 )
1140 }
1141 throws(
1142 () => toSchemaFromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12({ const: "a", enum: [{}] })),
1143 `Only primitive values are supported in "const" and "enum".\n at ["schema"]["enum"][0]`
1144 )
1145 })
1146
1147 it("imports built-in JSON Schema annotations", () => {
1148 assertFromJsonSchema(
1149 {
1150 schema: {
1151 type: "string",
1152 format: "email",
1153 contentEncoding: "base64",
1154 contentMediaType: "application/json",
1155 contentSchema: { type: "number" }
1156 }
1157 },
1158 {
1159 codes: makeCode(
1160 `Schema.String.annotate({ "format": "email", "contentEncoding": "base64", "contentMediaType": "application/json", "contentSchema": { "type": "number" } })`,
1161 `string`
1162 )
1163 }
1164 )
1165 })
1166
1167 describe("$ref", () => {
1168 it("rejects local references inside nested resources", () => {
1169 const reference = { $ref: "#/$defs/X" }
1170 const nested = { $id: "child", $defs: { X: { type: "number" } } }
1171 for (
1172 const [body, suffix] of [
1173 [reference, ""],
1174 [{ type: "object", properties: { value: reference } }, `["properties"]["value"]`],
1175 [{ properties: { value: reference } }, `["properties"]["value"]`],
1176 [{ type: "array", items: reference }, `["items"]`],
1177 [{ type: "array", prefixItems: [reference] }, `["prefixItems"][0]`],
1178 [{ allOf: [reference] }, `["allOf"][0]`],
1179 [{ anyOf: [reference, { type: "null" }] }, `["anyOf"][0]`],
1180 [{ oneOf: [reference, { type: "null" }] }, `["oneOf"][0]`],
1181 [{ type: "object", additionalProperties: reference }, `["additionalProperties"]`],
1182 [{ type: "object", patternProperties: { "^a": reference } }, `["patternProperties"]["^a"]`],
1183 [{ type: "object", propertyNames: reference }, `["propertyNames"]`]
1184 ] as const
1185 ) {
1186 throws(
1187 () =>
1188 toSchemaFromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12({
1189 $id: "https://example.com/root",
1190 type: "object",
1191 properties: { child: { ...nested, ...body } },
1192 $defs: { X: { type: "string" } }
1193 })),
1194 `Cannot resolve $ref under a nested "$id". Resolve or flatten it first.\n at ["schema"]["properties"]["child"]${suffix}["$ref"]`
1195 )
1196 }
1197 })
1198
1199 it("keeps root ids and nested resources without references supported", () => {
1200 assertFromJsonSchema({
1201 schema: {
1202 $id: "https://example.com/root",
1203 type: "object",
1204 properties: {
1205 child: { $id: "child", type: "number", $defs: { Unused: { $ref: "#/$defs/X" } } },
1206 value: { $ref: "#/$defs/X" }
1207 },
1208 $defs: { X: { type: "string" } }
1209 }
1210 }, {
1211 codes: makeCode(
1212 `Schema.StructWithRest(Schema.Struct({ "child": Schema.optionalKey(Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" }))), "value": Schema.optionalKey(X) }), [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))])`,
1213 `{ readonly "child"?: number, readonly "value"?: X } & { readonly [x: string]: Schema.Json }`
1214 ),
1215 references: {
1216 nonRecursives: [{ $ref: `X`, code: makeCode(`Schema.String.annotate({ "identifier": "X" })`, `string`) }]
1217 }
1218 })
1219 })
1220
1221 it("determines resource scope from onEnter results", () => {
1222 const document = JsonSchema.fromSchemaDraft2020_12({
1223 type: "object",
1224 properties: { child: { $id: "child", $ref: "#/$defs/X" } },
1225 $defs: { X: { type: "string" } }
1226 })
1227 const schema = toSchemaFromJsonSchemaDocument(document, {
1228 onEnter: (schema) => ({ ...schema, $id: undefined })
1229 })
1230 assertCode(schema, {
1231 codes: makeCode(
1232 `Schema.StructWithRest(Schema.Struct({ "child": Schema.optionalKey(X) }), [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))])`,
1233 `{ readonly "child"?: X } & { readonly [x: string]: Schema.Json }`
1234 ),
1235 references: {
1236 nonRecursives: [{ $ref: "X", code: makeCode(`Schema.String.annotate({ "identifier": "X" })`, `string`) }]
1237 }
1238 })
1239
1240 throws(
1241 () =>
1242 toSchemaFromJsonSchemaDocument(
1243 JsonSchema.fromSchemaDraft2020_12({
1244 type: "array",
1245 items: { $ref: "#/$defs/X" },
1246 $defs: { X: { type: "string" } }
1247 }),
1248 {
1249 onEnter: (schema) => schema.$ref === undefined ? schema : { ...schema, $id: "child" }
1250 }
1251 ),
1252 `Cannot resolve $ref under a nested "$id". Resolve or flatten it first.\n at ["schema"]["items"]["$ref"]`
1253 )
1254 })
1255
1256 it("rejects references in a reachable definition with its own id", () => {
1257 throws(
1258 () =>
1259 toSchemaFromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12({
1260 $ref: "#/$defs/Child",
1261 $defs: {
1262 Child: {
1263 $id: "https://example.com/child",
1264 type: "object",
1265 properties: { value: { $ref: "#/$defs/X" } }
1266 },
1267 X: { type: "string" }
1268 }
1269 })),
1270 `Cannot resolve $ref under a nested "$id". Resolve or flatten it first.\n at ["definitions"]["Child"]["properties"]["value"]["$ref"]`
1271 )
1272 })
1273
1274 it("rejects a reference below a definition instead of resolving its final token", () => {
1275 throws(
1276 () =>
1277 toSchemaFromJsonSchemaDocument(
1278 JsonSchema.fromSchemaDraft07({
1279 definitions: {
1280 inner: { type: "number" },
1281 outer: {
1282 type: "object",
1283 properties: {
1284 inner: { type: "string" }
1285 }
1286 }
1287 },
1288 type: "object",
1289 properties: {
1290 copy: { $ref: "#/definitions/outer/properties/inner" }
1291 }
1292 })
1293 ),
1294 `Unsupported $ref "#/$defs/outer/properties/inner". Use "#/$defs/Name".\n at ["schema"]["properties"]["copy"]["$ref"]`
1295 )
1296 })
1297
1298 it("rejects an empty reference", () => {
1299 throws(
1300 () => toSchemaFromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12({ $ref: "" })),
1301 `Unsupported $ref "". Use "#/$defs/Name".\n at ["schema"]["$ref"]`
1302 )
1303 })
1304
1305 it("rejects an external reference instead of aliasing a local definition", () => {
1306 throws(
1307 () =>
1308 toSchemaFromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12({
1309 $ref: "https://example.com/schema#/$defs/A",
1310 $defs: {
1311 A: { type: "string" }
1312 }
1313 })),
1314 `Unsupported $ref "https://example.com/schema#/$defs/A". Use "#/$defs/Name".\n at ["schema"]["$ref"]`
1315 )
1316 })
1317
1318 it("reports the full reference when a direct definition is missing", () => {
1319 throws(
1320 () => toSchemaFromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12({ $ref: "#/$defs/Missing" })),
1321 `Missing definition "Missing" for $ref "#/$defs/Missing".\n at ["schema"]["$ref"]`
1322 )
1323 })
1324
1325 it("unescapes a direct definition reference", () => {
1326 assertFromJsonSchema({
1327 schema: {
1328 $ref: "#/$defs/A~1B~0C",
1329 $defs: {
1330 "A/B~C": { type: "string" }
1331 }
1332 }
1333 }, {
1334 codes: makeCode(`A_B_C`, `A_B_C`),
1335 references: {
1336 nonRecursives: [{
1337 $ref: `A_B_C`,
1338 code: makeCode(`Schema.String.annotate({ "identifier": "A/B~C" })`, `string`)
1339 }]
1340 }
1341 })
1342 })
1343
1344 it("decodes a direct definition reference", () => {
1345 assertFromJsonSchema({
1346 schema: {
1347 $ref: "#/$defs/A%20B",
1348 $defs: {
1349 "A B": { type: "string" }
1350 }
1351 }
1352 }, {
1353 codes: makeCode(`A_B`, `A_B`),
1354 references: {
1355 nonRecursives: [{ $ref: `A_B`, code: makeCode(`Schema.String.annotate({ "identifier": "A B" })`, `string`) }]
1356 }
1357 })
1358 })
1359
1360 it("resolves a direct reference to an empty definition key", () => {
1361 assertFromJsonSchema({
1362 schema: {
1363 $ref: "#/$defs/",
1364 $defs: {
1365 "": { type: "string" }
1366 }
1367 }
1368 }, {
1369 codes: makeCode(`_`, `_`),
1370 references: {
1371 nonRecursives: [{ $ref: `_`, code: makeCode(`Schema.String.annotate({ "identifier": "" })`, `string`) }]
1372 }
1373 })
1374 })
1375
1376 it("should create a Reference and a definition", () => {
1377 assertFromJsonSchema(
1378 {
1379 schema: {
1380 $ref: "#/$defs/A",
1381 $defs: {
1382 A: {
1383 type: "string"
1384 }
1385 }
1386 }
1387 },
1388 {
1389 codes: makeCode(`A`, `A`),
1390 references: {
1391 nonRecursives: [{ $ref: `A`, code: makeCode(`Schema.String.annotate({ "identifier": "A" })`, `string`) }]
1392 }
1393 }
1394 )
1395 })
1396
1397 it("should preserve an annotated $ref as a stable suspend", () => {
1398 assertFromJsonSchema(
1399 {
1400 schema: {
1401 $ref: "#/$defs/A",
1402 description: "a",
1403 $defs: {
1404 A: {
1405 type: "string"
1406 }
1407 }
1408 }
1409 },
1410 {
1411 codes: makeCode(`Schema.suspend((): Schema.Codec<A> => A).annotate({ "description": "a" })`, `A`),
1412 references: {
1413 nonRecursives: [{ $ref: `A`, code: makeCode(`Schema.String.annotate({ "identifier": "A" })`, `string`) }]
1414 }
1415 }
1416 )
1417 })
1418
1419 it("does not combine annotation siblings with a $ref", () => {
1420 assertFromJsonSchema({
1421 schema: {
1422 $ref: "#/$defs/A",
1423 format: "custom",
1424 $defs: {
1425 A: { type: "number" }
1426 }
1427 }
1428 }, {
1429 codes: makeCode(`Schema.suspend((): Schema.Codec<A> => A).annotate({ "format": "custom" })`, `A`),
1430 references: {
1431 nonRecursives: [{
1432 $ref: `A`,
1433 code: makeCode(
1434 `Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number", "identifier": "A" }))`,
1435 `number`
1436 )
1437 }]
1438 }
1439 })
1440 })
1441
1442 it("should preserve a $ref refined only by annotations as a stable suspend", () => {
1443 assertFromJsonSchema(
1444 {
1445 schema: {
1446 allOf: [
1447 { $ref: "#/$defs/A" },
1448 { description: "a" }
1449 ],
1450 $defs: {
1451 A: {
1452 type: "string"
1453 }
1454 }
1455 }
1456 },
1457 {
1458 codes: makeCode(`Schema.suspend((): Schema.Codec<A> => A).annotate({ "description": "a" })`, `A`),
1459 references: {
1460 nonRecursives: [{ $ref: `A`, code: makeCode(`Schema.String.annotate({ "identifier": "A" })`, `string`) }]
1461 }
1462 }
1463 )
1464 })
1465
1466 it("recursive schema", () => {
1467 assertFromJsonSchema(
1468 {
1469 schema: {
1470 $ref: "#/$defs/A",
1471 $defs: {
1472 A: {
1473 type: "object",
1474 properties: {
1475 name: {
1476 type: "string"
1477 },
1478 children: {
1479 type: "array",
1480 items: {
1481 $ref: "#/$defs/A"
1482 }
1483 }
1484 },
1485 required: [
1486 "name",
1487 "children"
1488 ],
1489 additionalProperties: false
1490 }
1491 }
1492 }
1493 },
1494 {
1495 codes: makeCode(`A`, `A`),
1496 references: {
1497 recursives: {
1498 A: makeCode(
1499 `Schema.Struct({ "name": Schema.String, "children": Schema.Array(Schema.suspend((): Schema.Codec<A> => A)) }).annotate({ "identifier": "A" })`,
1500 `{ readonly "name": string, readonly "children": ReadonlyArray<A> }`
1501 )
1502 }
1503 }
1504 }
1505 )
1506 })
1507
1508 it("preserves annotations on a recursive $ref", () => {
1509 assertFromJsonSchema(
1510 {
1511 schema: {
1512 $ref: "#/$defs/Node",
1513 $defs: {
1514 Node: {
1515 type: "object",
1516 properties: {
1517 child: {
1518 $ref: "#/$defs/Node",
1519 description: "recursive child"
1520 }
1521 },
1522 required: ["child"],
1523 additionalProperties: false
1524 }
1525 }
1526 }
1527 },
1528 {
1529 codes: makeCode(`Node`, `Node`),
1530 references: {
1531 recursives: {
1532 Node: makeCode(
1533 `Schema.Struct({ "child": Schema.suspend((): Schema.Codec<Node> => Node).annotate({ "description": "recursive child" }) }).annotate({ "identifier": "Node" })`,
1534 `{ readonly "child": Node }`
1535 )
1536 }
1537 }
1538 }
1539 )
1540 })
1541
1542 it("combines assertion siblings with a $ref", () => {
1543 assertFromJsonSchema({
1544 schema: {
1545 $ref: "#/$defs/Name",
1546 minLength: 2,
1547 description: "name",
1548 $defs: {
1549 Name: { type: "string" }
1550 }
1551 }
1552 }, {
1553 codes: makeCode(
1554 `Schema.String.annotate({ "description": "name" }).check(Schema.isMinCodePoints(2).annotate({ "expected": "a string with at least 2 code points" }))`,
1555 `string`
1556 )
1557 })
1558 })
1559
1560 it("rejects assertion siblings on a recursive $ref", () => {
1561 throws(
1562 () =>
1563 SchemaRepresentation.fromJsonSchemaDocument(
1564 JsonSchema.fromSchemaDraft2020_12({
1565 $ref: "#/$defs/Node",
1566 $defs: {
1567 Node: {
1568 type: "object",
1569 properties: {
1570 child: {
1571 $ref: "#/$defs/Node",
1572 minProperties: 1
1573 }
1574 }
1575 }
1576 }
1577 })
1578 ),
1579 `Recursive $ref "Node" cannot have sibling constraints.\n at ["definitions"]["Node"]["properties"]["child"]["$ref"]`
1580 )
1581 })
1582 })
1583
1584 describe("allOf", () => {
1585 it("prunes disjoint lanes before intersecting unions", () => {
1586 assertFromJsonSchema({
1587 schema: {
1588 allOf: [
1589 { anyOf: [{ type: "string" }, { type: "number" }] },
1590 { anyOf: [{ type: "number" }, { type: "boolean" }] }
1591 ]
1592 }
1593 }, {
1594 codes: makeCode(`Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" }))`, `number`)
1595 })
1596 })
1597
1598 it("rejects intersections requiring a Cartesian product", () => {
1599 throws(
1600 () =>
1601 toSchemaFromJsonSchemaDocument(
1602 JsonSchema.fromSchemaDraft2020_12({
1603 allOf: [
1604 {
1605 anyOf: [
1606 { type: "string", minLength: 2, maxLength: 3 },
1607 { type: "string", minLength: 5, maxLength: 6 }
1608 ]
1609 },
1610 {
1611 anyOf: [
1612 { type: "string", pattern: "^a" },
1613 { type: "string", pattern: "z$" }
1614 ]
1615 }
1616 ]
1617 })
1618 ),
1619 `Cannot intersect these "anyOf" or "oneOf" alternatives without expanding their branches.\n at ["schema"]["allOf"][1]`
1620 )
1621 })
1622
1623 it("rejects distributions that duplicate a nested choice", () => {
1624 throws(
1625 () =>
1626 toSchemaFromJsonSchemaDocument(
1627 JsonSchema.fromSchemaDraft2020_12({
1628 allOf: [
1629 {
1630 anyOf: [
1631 { type: "object", properties: { tag: { const: "a" } } },
1632 { type: "object", properties: { tag: { const: "b" } } }
1633 ]
1634 },
1635 {
1636 type: "object",
1637 properties: {
1638 value: { anyOf: [{ type: "string" }, { type: "number" }] }
1639 }
1640 }
1641 ]
1642 })
1643 ),
1644 `Cannot intersect these "anyOf" or "oneOf" alternatives without expanding their branches.\n at ["schema"]["allOf"][1]`
1645 )
1646 })
1647
1648 it("distributes across a nested reference without choices", () => {
1649 assertFromJsonSchema({
1650 schema: {
1651 allOf: [
1652 {
1653 anyOf: [
1654 { type: "object", required: ["a"] },
1655 { type: "object", required: ["b"] }
1656 ]
1657 },
1658 {
1659 type: "object",
1660 properties: {
1661 value: { $ref: "#/$defs/Value" }
1662 },
1663 required: ["value"]
1664 }
1665 ],
1666 $defs: {
1667 Value: { type: "string" }
1668 }
1669 }
1670 }, {
1671 codes: makeCode(
1672 `Schema.Union([Schema.StructWithRest(Schema.Struct({ "a": Schema.Json.annotate({ "expected": "JSON value" }), "value": Value }), [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))]), Schema.StructWithRest(Schema.Struct({ "b": Schema.Json.annotate({ "expected": "JSON value" }), "value": Value }), [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))])])`,
1673 `{ readonly "a": Schema.Json, readonly "value": Value } & { readonly [x: string]: Schema.Json } | { readonly "b": Schema.Json, readonly "value": Value } & { readonly [x: string]: Schema.Json }`
1674 ),
1675 references: {
1676 nonRecursives: [{
1677 $ref: `Value`,
1678 code: makeCode(`Schema.String.annotate({ "identifier": "Value" })`, `string`)
1679 }]
1680 }
1681 })
1682 })
1683
1684 it("reports unsupported keywords after a false schema", () => {
1685 throws(
1686 () =>
1687 toSchemaFromJsonSchemaDocument(
1688 JsonSchema.fromSchemaDraft2020_12({
1689 allOf: [false, { contains: {} }]
1690 })
1691 ),
1692 `Cannot import JSON Schema keyword "contains": Effect has no equivalent constraint.\n at ["schema"]["allOf"][1]["contains"]`
1693 )
1694 })
1695
1696 it("intersects one constraint with many alternatives linearly", () => {
1697 assertFromJsonSchema({
1698 schema: {
1699 pattern: "^value-",
1700 anyOf: Array.from({ length: 16 }, (_, index) => ({ const: `value-${index}` }))
1701 }
1702 }, {
1703 codes: makeCode(
1704 `Schema.Literals(["value-0", "value-1", "value-2", "value-3", "value-4", "value-5", "value-6", "value-7", "value-8", "value-9", "value-10", "value-11", "value-12", "value-13", "value-14", "value-15"])`,
1705 `"value-0" | "value-1" | "value-2" | "value-3" | "value-4" | "value-5" | "value-6" | "value-7" | "value-8" | "value-9" | "value-10" | "value-11" | "value-12" | "value-13" | "value-14" | "value-15"`
1706 )
1707 })
1708 })
1709
1710 it("resolves a root reference before intersecting allOf", () => {
1711 const definition: JsonSchema.JsonSchema = { type: "string", minLength: 1 }
1712 assertFromJsonSchema({
1713 schema: {
1714 $ref: "#/$defs/A",
1715 allOf: [{ type: "string", maxLength: 2 }],
1716 $defs: { A: definition }
1717 }
1718 }, {
1719 codes: makeCode(
1720 `Schema.String.check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })).check(Schema.isMaxCodePoints(2).annotate({ "expected": "a string with at most 2 code points" }))`,
1721 `string`
1722 )
1723 })
1724 })
1725
1726 it("resolves a reference declared inside allOf", () => {
1727 const definition: JsonSchema.JsonSchema = { type: "string", minLength: 1 }
1728 assertFromJsonSchema({
1729 schema: {
1730 allOf: [{ $ref: "#/$defs/A" }, { type: "string", maxLength: 2 }],
1731 $defs: { A: definition }
1732 }
1733 }, {
1734 codes: makeCode(
1735 `Schema.String.check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })).check(Schema.isMaxCodePoints(2).annotate({ "expected": "a string with at most 2 code points" }))`,
1736 `string`
1737 )
1738 })
1739 })
1740
1741 it("preserves annotations on array intersections", () => {
1742 assertFromJsonSchema(
1743 {
1744 schema: {
1745 type: "array",
1746 allOf: [{ description: "a" }]
1747 }
1748 },
1749 {
1750 codes: makeCode(
1751 `Schema.Array(Schema.Json.annotate({ "expected": "JSON value" })).annotate({ "description": "a" })`,
1752 `ReadonlyArray<Schema.Json>`
1753 )
1754 }
1755 )
1756 })
1757
1758 it("preserves annotations on object intersections", () => {
1759 assertFromJsonSchema(
1760 {
1761 schema: {
1762 type: "object",
1763 additionalProperties: false,
1764 allOf: [{ description: "a" }]
1765 }
1766 },
1767 {
1768 codes: makeCode(
1769 `Schema.Record(Schema.String, Schema.Never).annotate({ "description": "a" })`,
1770 `{ readonly [x: string]: never }`
1771 )
1772 }
1773 )
1774 })
1775
1776 it("returns Never when a union has no compatible member", () => {
1777 assertFromJsonSchema(
1778 {
1779 schema: {
1780 type: "string",
1781 allOf: [{ anyOf: [{ type: "number" }, { type: "boolean" }] }]
1782 }
1783 },
1784 { codes: makeCode(`Schema.Never`, `never`) }
1785 )
1786 })
1787
1788 function assertLiteralRefinement(
1789 refinement: JsonSchema.JsonSchema,
1790 valid: string | number,
1791 invalid: string | number
1792 ) {
1793 for (const literal of [valid, invalid]) {
1794 for (const allOf of [[refinement, { const: literal }], [{ const: literal }, refinement]]) {
1795 const value = JSON.stringify(literal)
1796 assertFromJsonSchema(
1797 { schema: { allOf } },
1798 {
1799 codes: literal === valid ? makeCode(`Schema.Literal(${value})`, value) : makeCode(`Schema.Never`, `never`)
1800 }
1801 )
1802 }
1803 }
1804 }
1805
1806 describe("literal refinements", () => {
1807 it("minLength", () => {
1808 assertLiteralRefinement({ type: "string", minLength: 2 }, "ab", "a")
1809 })
1810
1811 it("maxLength", () => {
1812 assertLiteralRefinement({ type: "string", maxLength: 1 }, "a", "ab")
1813 })
1814
1815 it("pattern", () => {
1816 assertLiteralRefinement({ type: "string", pattern: "^a+$" }, "aa", "ab")
1817 })
1818
1819 it("integer", () => {
1820 assertLiteralRefinement({ type: "integer" }, 1, 1.5)
1821 })
1822
1823 it("multipleOf", () => {
1824 assertLiteralRefinement({ type: "number", multipleOf: 0.1 }, 0.3, 0.31)
1825 })
1826
1827 it("multipleOf beyond the toFixed precision limit", () => {
1828 assertLiteralRefinement({ type: "number", multipleOf: Number("1e-101") }, 0, Number("5e-102"))
1829 })
1830
1831 it("multipleOf with a large scientific operand", () => {
1832 assertLiteralRefinement({ type: "number", multipleOf: 2 }, Number("1e21"), 1)
1833 })
1834
1835 it("multipleOf with a nonzero subnormal remainder", () => {
1836 assertLiteralRefinement({ type: "number", multipleOf: Number("1e-323") }, 0, Number("1.042e-321"))
1837 })
1838
1839 it("minimum", () => {
1840 assertLiteralRefinement({ type: "number", minimum: 1 }, 1, 0)
1841 })
1842
1843 it("maximum", () => {
1844 assertLiteralRefinement({ type: "number", maximum: 1 }, 1, 2)
1845 })
1846
1847 it("exclusiveMinimum", () => {
1848 assertLiteralRefinement({ type: "number", exclusiveMinimum: 1 }, 2, 1)
1849 })
1850
1851 it("exclusiveMaximum", () => {
1852 assertLiteralRefinement({ type: "number", exclusiveMaximum: 1 }, 0, 1)
1853 })
1854
1855 it("filter group", () => {
1856 assertLiteralRefinement(
1857 { type: "number", allOf: [{ minimum: 1, maximum: 2, description: "range" }] },
1858 2,
1859 0
1860 )
1861 })
1862
1863 it("filters enum members when the refinement precedes the enum", () => {
1864 assertFromJsonSchema(
1865 { schema: { type: "string", minLength: 2, allOf: [{ enum: ["a", "ab"] }] } },
1866 { codes: makeCode(`Schema.Literal("ab")`, `"ab"`) }
1867 )
1868 })
1869
1870 it("filters enum members when the enum precedes the refinement", () => {
1871 assertFromJsonSchema(
1872 { schema: { enum: ["a", "ab"], allOf: [{ type: "string", minLength: 2 }] } },
1873 { codes: makeCode(`Schema.Literal("ab")`, `"ab"`) }
1874 )
1875 })
1876 })
1877
1878 it("no type", () => {
1879 assertFromJsonSchema(
1880 {
1881 schema: {
1882 allOf: [
1883 { type: "string" }
1884 ]
1885 }
1886 },
1887 { codes: makeCode(`Schema.String`, `string`) }
1888 )
1889 })
1890
1891 describe("type: string", () => {
1892 it("& minLength", () => {
1893 assertFromJsonSchema(
1894 {
1895 schema: {
1896 type: "string",
1897 allOf: [
1898 { minLength: 1 }
1899 ]
1900 }
1901 },
1902 {
1903 codes: makeCode(
1904 `Schema.String.check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }))`,
1905 `string`
1906 )
1907 }
1908 )
1909 })
1910
1911 it("& minLength + description", () => {
1912 assertFromJsonSchema(
1913 {
1914 schema: {
1915 type: "string",
1916 allOf: [
1917 { minLength: 1, description: "b" }
1918 ]
1919 }
1920 },
1921 {
1922 codes: makeCode(
1923 `Schema.String.check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1", "description": "b" }))`,
1924 `string`
1925 )
1926 }
1927 )
1928 })
1929
1930 it("description & minLength", () => {
1931 assertFromJsonSchema(
1932 {
1933 schema: {
1934 type: "string",
1935 description: "a",
1936 allOf: [
1937 { minLength: 1 }
1938 ]
1939 }
1940 },
1941 {
1942 codes: makeCode(
1943 `Schema.String.annotate({ "description": "a" }).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }))`,
1944 `string`
1945 )
1946 }
1947 )
1948 })
1949
1950 it("description & description", () => {
1951 assertFromJsonSchema(
1952 {
1953 schema: {
1954 type: "string",
1955 description: "a",
1956 allOf: [
1957 { description: "b" }
1958 ]
1959 }
1960 },
1961 { codes: makeCode(`Schema.String.annotate({ "description": "b" })`, `string`) }
1962 )
1963 })
1964
1965 it("description & minLength + description", () => {
1966 assertFromJsonSchema(
1967 {
1968 schema: {
1969 type: "string",
1970 description: "a",
1971 allOf: [
1972 { minLength: 1, description: "b" }
1973 ]
1974 }
1975 },
1976 {
1977 codes: makeCode(
1978 `Schema.String.annotate({ "description": "a" }).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1", "description": "b" }))`,
1979 `string`
1980 )
1981 }
1982 )
1983 })
1984
1985 it("maxLength & minLength", () => {
1986 assertFromJsonSchema(
1987 {
1988 schema: {
1989 type: "string",
1990 maxLength: 2,
1991 allOf: [
1992 { minLength: 1 }
1993 ]
1994 }
1995 },
1996 {
1997 codes: makeCode(
1998 `Schema.String.check(Schema.isMaxCodePoints(2).annotate({ "expected": "a string with at most 2 code points" })).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }))`,
1999 `string`
2000 )
2001 }
2002 )
2003 })
2004
2005 it("description + maxLength & minLength", () => {
2006 assertFromJsonSchema(
2007 {
2008 schema: {
2009 type: "string",
2010 description: "a",
2011 maxLength: 2,
2012 allOf: [
2013 { minLength: 1 }
2014 ]
2015 }
2016 },
2017 {
2018 codes: makeCode(
2019 `Schema.String.annotate({ "description": "a" }).check(Schema.isMaxCodePoints(2).annotate({ "expected": "a string with at most 2 code points" })).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }))`,
2020 `string`
2021 )
2022 }
2023 )
2024 })
2025
2026 it("description + maxLength & minLength + description", () => {
2027 assertFromJsonSchema(
2028 {
2029 schema: {
2030 type: "string",
2031 description: "a",
2032 maxLength: 2,
2033 allOf: [
2034 { minLength: 1, description: "b" }
2035 ]
2036 }
2037 },
2038 {
2039 codes: makeCode(
2040 `Schema.String.annotate({ "description": "a" }).check(Schema.isMaxCodePoints(2).annotate({ "expected": "a string with at most 2 code points" })).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1", "description": "b" }))`,
2041 `string`
2042 )
2043 }
2044 )
2045 })
2046
2047 it("& minLength + maxLength", () => {
2048 assertFromJsonSchema(
2049 {
2050 schema: {
2051 type: "string",
2052 allOf: [
2053 { minLength: 1, maxLength: 2 }
2054 ]
2055 }
2056 },
2057 {
2058 codes: makeCode(
2059 `Schema.String.check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })).check(Schema.isMaxCodePoints(2).annotate({ "expected": "a string with at most 2 code points" }))`,
2060 `string`
2061 )
2062 }
2063 )
2064 })
2065
2066 it("& minLength + maxLength + description", () => {
2067 assertFromJsonSchema(
2068 {
2069 schema: {
2070 type: "string",
2071 allOf: [
2072 { minLength: 1, maxLength: 2, description: "b" }
2073 ]
2074 }
2075 },
2076 {
2077 codes: makeCode(
2078 `Schema.String.check(Schema.makeFilterGroup([Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }), Schema.isMaxCodePoints(2).annotate({ "expected": "a string with at most 2 code points" })]).annotate({ "description": "b" }))`,
2079 `string`
2080 )
2081 }
2082 )
2083 })
2084
2085 it("& (minLength & maxLength + description)", () => {
2086 assertFromJsonSchema(
2087 {
2088 schema: {
2089 type: "string",
2090 allOf: [
2091 { minLength: 1, allOf: [{ maxLength: 2, description: "c" }] }
2092 ]
2093 }
2094 },
2095 {
2096 codes: makeCode(
2097 `Schema.String.check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })).check(Schema.isMaxCodePoints(2).annotate({ "expected": "a string with at most 2 code points", "description": "c" }))`,
2098 `string`
2099 )
2100 }
2101 )
2102 })
2103
2104 it("& (minLength + description & maxLength + description)", () => {
2105 assertFromJsonSchema(
2106 {
2107 schema: {
2108 type: "string",
2109 allOf: [
2110 { minLength: 1, description: "b", allOf: [{ maxLength: 2, description: "c" }] }
2111 ]
2112 }
2113 },
2114 {
2115 codes: makeCode(
2116 `Schema.String.check(Schema.makeFilterGroup([Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }), Schema.isMaxCodePoints(2).annotate({ "expected": "a string with at most 2 code points", "description": "c" })]).annotate({ "description": "b" }))`,
2117 `string`
2118 )
2119 }
2120 )
2121 })
2122
2123 it("intersects with a single-member string enum", () => {
2124 assertFromJsonSchema(
2125 {
2126 schema: {
2127 type: "string",
2128 allOf: [
2129 { enum: ["a"] }
2130 ]
2131 }
2132 },
2133 { codes: makeCode(`Schema.Literal("a")`, `"a"`) }
2134 )
2135 assertFromJsonSchema(
2136 {
2137 schema: {
2138 type: "string",
2139 description: "a",
2140 allOf: [
2141 { enum: ["a"], description: "b" }
2142 ]
2143 }
2144 },
2145 { codes: makeCode(`Schema.Literal("a").annotate({ "description": "b" })`, `"a"`) }
2146 )
2147 })
2148
2149 it("intersects with a multi-member string enum", () => {
2150 assertFromJsonSchema(
2151 {
2152 schema: {
2153 type: "string",
2154 allOf: [
2155 { enum: ["a", "b"] }
2156 ]
2157 }
2158 },
2159 { codes: makeCode(`Schema.Literals(["a", "b"])`, `"a" | "b"`) }
2160 )
2161 })
2162
2163 it("& mixed enum", () => {
2164 assertFromJsonSchema(
2165 {
2166 schema: {
2167 type: "string",
2168 allOf: [
2169 { enum: ["a", 1] }
2170 ]
2171 }
2172 },
2173 { codes: makeCode(`Schema.Literal("a")`, `"a"`) }
2174 )
2175 })
2176 })
2177
2178 describe("type: number", () => {
2179 it("number & number preserves annotations after removing duplicate checks", () => {
2180 assertFromJsonSchema(
2181 {
2182 schema: {
2183 type: "number",
2184 allOf: [{ type: "number", description: "b" }]
2185 }
2186 },
2187 {
2188 codes: makeCode(
2189 `Schema.Number.annotate({ "description": "b" }).check(Schema.isFinite().annotate({ "expected": "a finite number" }))`,
2190 `number`
2191 )
2192 }
2193 )
2194 })
2195
2196 it("number & integer", () => {
2197 assertFromJsonSchema(
2198 {
2199 schema: {
2200 type: "number",
2201 allOf: [
2202 { type: "integer" }
2203 ]
2204 }
2205 },
2206 {
2207 codes: makeCode(
2208 `Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })).check(Schema.isInt().annotate({ "expected": "an integer" }))`,
2209 `number`
2210 )
2211 }
2212 )
2213 })
2214
2215 it("number & integer & integer", () => {
2216 assertFromJsonSchema(
2217 {
2218 schema: {
2219 type: "number",
2220 allOf: [
2221 { type: "integer", minimum: 2 },
2222 { type: "integer", maximum: 2 }
2223 ]
2224 }
2225 },
2226 {
2227 codes: makeCode(
2228 `Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })).check(Schema.isInt().annotate({ "expected": "an integer" })).check(Schema.isGreaterThanOrEqualTo(2).annotate({ "expected": "a value greater than or equal to 2" })).check(Schema.isLessThanOrEqualTo(2).annotate({ "expected": "a value less than or equal to 2" }))`,
2229 `number`
2230 )
2231 }
2232 )
2233 })
2234
2235 it("integer & number", () => {
2236 assertFromJsonSchema(
2237 {
2238 schema: {
2239 type: "integer",
2240 allOf: [
2241 { type: "number" }
2242 ]
2243 }
2244 },
2245 {
2246 codes: makeCode(
2247 `Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check(Schema.isFinite().annotate({ "expected": "a finite number" }))`,
2248 `number`
2249 )
2250 }
2251 )
2252 })
2253
2254 it("& (minimum + description & maximum + description)", () => {
2255 assertFromJsonSchema(
2256 {
2257 schema: {
2258 type: "number",
2259 allOf: [
2260 { minimum: 1, description: "b", allOf: [{ maximum: 2, description: "c" }] }
2261 ]
2262 }
2263 },
2264 {
2265 codes: makeCode(
2266 `Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })).check(Schema.makeFilterGroup([Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" }), Schema.isLessThanOrEqualTo(2).annotate({ "expected": "a value less than or equal to 2", "description": "c" })]).annotate({ "description": "b" }))`,
2267 `number`
2268 )
2269 }
2270 )
2271 })
2272
2273 it("continues intersecting after an annotated filter group", () => {
2274 assertFromJsonSchema(
2275 {
2276 schema: {
2277 type: "number",
2278 allOf: [
2279 { minimum: 1, maximum: 2, description: "range" },
2280 { type: "integer" }
2281 ]
2282 }
2283 },
2284 {
2285 codes: makeCode(
2286 `Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })).check(Schema.makeFilterGroup([Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" }), Schema.isLessThanOrEqualTo(2).annotate({ "expected": "a value less than or equal to 2" })]).annotate({ "description": "range" })).check(Schema.isInt().annotate({ "expected": "an integer" }))`,
2287 `number`
2288 )
2289 }
2290 )
2291 })
2292
2293 it("intersects with a single-member number enum", () => {
2294 assertFromJsonSchema(
2295 {
2296 schema: {
2297 type: "number",
2298 allOf: [
2299 { enum: [1] }
2300 ]
2301 }
2302 },
2303 { codes: makeCode(`Schema.Literal(1)`, `1`) }
2304 )
2305 assertFromJsonSchema(
2306 {
2307 schema: {
2308 type: "number",
2309 description: "a",
2310 allOf: [
2311 { enum: [1], description: "b" }
2312 ]
2313 }
2314 },
2315 { codes: makeCode(`Schema.Literal(1).annotate({ "description": "b" })`, `1`) }
2316 )
2317 })
2318
2319 it("intersects with a multi-member number enum", () => {
2320 assertFromJsonSchema(
2321 {
2322 schema: {
2323 type: "number",
2324 allOf: [
2325 { enum: [1, 2] }
2326 ]
2327 }
2328 },
2329 { codes: makeCode(`Schema.Literals([1, 2])`, `1 | 2`) }
2330 )
2331 })
2332 })
2333
2334 describe("type: boolean", () => {
2335 it("boolean & boolean", () => {
2336 assertFromJsonSchema(
2337 {
2338 schema: {
2339 type: "boolean",
2340 allOf: [{ type: "boolean" }]
2341 }
2342 },
2343 { codes: makeCode(`Schema.Boolean`, `boolean`) }
2344 )
2345 })
2346
2347 it("boolean & non-boolean literal", () => {
2348 assertFromJsonSchema(
2349 {
2350 schema: {
2351 type: "boolean",
2352 allOf: [{ const: 1 }]
2353 }
2354 },
2355 { codes: makeCode(`Schema.Never`, `never`) }
2356 )
2357 })
2358
2359 it("intersects with a single-member boolean enum", () => {
2360 assertFromJsonSchema(
2361 {
2362 schema: {
2363 type: "boolean",
2364 allOf: [
2365 { enum: [true] }
2366 ]
2367 }
2368 },
2369 { codes: makeCode(`Schema.Literal(true)`, `true`) }
2370 )
2371 assertFromJsonSchema(
2372 {
2373 schema: {
2374 type: "boolean",
2375 description: "a",
2376 allOf: [
2377 { enum: [true], description: "b" }
2378 ]
2379 }
2380 },
2381 { codes: makeCode(`Schema.Literal(true).annotate({ "description": "b" })`, `true`) }
2382 )
2383 })
2384 })
2385
2386 describe("type: array", () => {
2387 function assertArrayAllOf(
2388 a: JsonSchema.JsonSchema,
2389 b: JsonSchema.JsonSchema,
2390 expected: Expected
2391 ) {
2392 for (const [schema, member] of [[a, b], [b, a]]) {
2393 assertFromJsonSchema(
2394 { schema: { ...schema, allOf: [member] } },
2395 expected
2396 )
2397 }
2398 }
2399
2400 it("uniqueItems & uniqueItems", () => {
2401 assertFromJsonSchema(
2402 {
2403 schema: {
2404 type: "array",
2405 uniqueItems: true,
2406 allOf: [
2407 { uniqueItems: true }
2408 ]
2409 }
2410 },
2411 {
2412 codes: makeCode(
2413 `Schema.Array(Schema.Json.annotate({ "expected": "JSON value" })).check(Schema.isUnique().annotate({ "expected": "an array with unique items" }))`,
2414 `ReadonlyArray<Schema.Json>`
2415 )
2416 }
2417 )
2418 })
2419
2420 it("combines unequal open prefixes", () => {
2421 assertArrayAllOf(
2422 {
2423 type: "array",
2424 prefixItems: [{ type: "string" }],
2425 minItems: 1,
2426 items: { type: "string" }
2427 },
2428 {
2429 type: "array",
2430 prefixItems: [{ type: "string" }, { const: "tail" }],
2431 minItems: 2,
2432 items: { type: "string" }
2433 },
2434 {
2435 codes: makeCode(
2436 `Schema.TupleWithRest(Schema.Tuple([Schema.String, Schema.Literal("tail")]), [Schema.String])`,
2437 `readonly [string, "tail", ...Array<string>]`
2438 )
2439 }
2440 )
2441 })
2442
2443 it("truncates optional elements forbidden by a closed tuple", () => {
2444 assertArrayAllOf(
2445 {
2446 type: "array",
2447 prefixItems: [{ type: "string" }],
2448 minItems: 1,
2449 maxItems: 1
2450 },
2451 {
2452 type: "array",
2453 prefixItems: [{ type: "string" }, { type: "number" }],
2454 minItems: 1,
2455 maxItems: 2
2456 },
2457 { codes: makeCode(`Schema.Tuple([Schema.String])`, `readonly [string]`) }
2458 )
2459 })
2460
2461 it("returns Never when a closed tuple forbids a required element", () => {
2462 assertArrayAllOf(
2463 {
2464 type: "array",
2465 prefixItems: [{ type: "string" }],
2466 minItems: 1,
2467 maxItems: 1
2468 },
2469 {
2470 type: "array",
2471 prefixItems: [{ type: "string" }, { type: "number" }],
2472 minItems: 2,
2473 maxItems: 2
2474 },
2475 { codes: makeCode(`Schema.Never`, `never`) }
2476 )
2477 })
2478
2479 it("combines an open rest with a closed rest", () => {
2480 assertArrayAllOf(
2481 {
2482 type: "array",
2483 prefixItems: [{ type: "string" }],
2484 minItems: 1,
2485 items: { type: "number" }
2486 },
2487 {
2488 type: "array",
2489 prefixItems: [{ type: "string" }],
2490 minItems: 1,
2491 maxItems: 1
2492 },
2493 { codes: makeCode(`Schema.Tuple([Schema.String])`, `readonly [string]`) }
2494 )
2495 })
2496
2497 it("closes the tuple when the rest intersection is Never", () => {
2498 assertArrayAllOf(
2499 {
2500 type: "array",
2501 prefixItems: [{ type: "string" }],
2502 minItems: 1,
2503 items: { type: "string" }
2504 },
2505 {
2506 type: "array",
2507 prefixItems: [{ type: "string" }],
2508 minItems: 1,
2509 items: { type: "number" }
2510 },
2511 { codes: makeCode(`Schema.Tuple([Schema.String])`, `readonly [string]`) }
2512 )
2513 })
2514
2515 it("rejects a required literal that fails rest refinements", () => {
2516 assertArrayAllOf(
2517 {
2518 type: "array",
2519 minItems: 1,
2520 items: { const: 0 }
2521 },
2522 {
2523 type: "array",
2524 prefixItems: [{ type: "number", minimum: 1 }],
2525 minItems: 1,
2526 maxItems: 1
2527 },
2528 { codes: makeCode(`Schema.Never`, `never`) }
2529 )
2530 })
2531
2532 it("truncates an optional literal that fails rest refinements", () => {
2533 assertArrayAllOf(
2534 {
2535 type: "array",
2536 items: { const: 0 }
2537 },
2538 {
2539 type: "array",
2540 prefixItems: [{ type: "number", minimum: 1 }],
2541 maxItems: 1
2542 },
2543 { codes: makeCode(`Schema.Tuple([])`, `readonly []`) }
2544 )
2545 })
2546
2547 it("preserves a literal that satisfies rest refinements", () => {
2548 assertArrayAllOf(
2549 {
2550 type: "array",
2551 minItems: 1,
2552 items: { const: 2 }
2553 },
2554 {
2555 type: "array",
2556 prefixItems: [{ type: "number", minimum: 1 }],
2557 minItems: 1,
2558 maxItems: 1
2559 },
2560 {
2561 codes: makeCode(
2562 `Schema.Tuple([Schema.Literal(2)]).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }))`,
2563 `readonly [2]`
2564 )
2565 }
2566 )
2567 })
2568 })
2569
2570 it("short-circuits false intersections to Never", () => {
2571 assertFromJsonSchema(
2572 { schema: { allOf: [false, { type: "string" }] } },
2573 { codes: makeCode(`Schema.Never`, `never`) }
2574 )
2575 })
2576
2577 it("returns Never when intersecting array and string", () => {
2578 assertFromJsonSchema(
2579 { schema: { allOf: [{ type: "array" }, { type: "string" }] } },
2580 { codes: makeCode(`Schema.Never`, `never`) }
2581 )
2582 })
2583
2584 it("returns Never when intersecting object and string", () => {
2585 assertFromJsonSchema(
2586 { schema: { allOf: [{ type: "object" }, { type: "string" }] } },
2587 { codes: makeCode(`Schema.Never`, `never`) }
2588 )
2589 })
2590
2591 it("returns Never when intersecting null and string", () => {
2592 assertFromJsonSchema(
2593 { schema: { allOf: [{ type: "null" }, { type: "string" }] } },
2594 { codes: makeCode(`Schema.Never`, `never`) }
2595 )
2596 })
2597
2598 it("returns Never when intersecting distinct literals", () => {
2599 assertFromJsonSchema(
2600 { schema: { allOf: [{ const: 1 }, { const: 2 }] } },
2601 { codes: makeCode(`Schema.Never`, `never`) }
2602 )
2603 })
2604
2605 it("preserves null when intersecting null types", () => {
2606 assertFromJsonSchema(
2607 { schema: { allOf: [{ type: "null" }, { type: "null" }] } },
2608 { codes: makeCode(`Schema.Null`, `null`) }
2609 )
2610 })
2611
2612 it("preserves a literal when intersecting identical literals", () => {
2613 assertFromJsonSchema(
2614 { schema: { allOf: [{ const: 1 }, { const: 1 }] } },
2615 { codes: makeCode(`Schema.Literal(1)`, `1`) }
2616 )
2617 })
2618
2619 it("preserves a matching literal after a boolean type", () => {
2620 assertFromJsonSchema(
2621 { schema: { allOf: [{ type: "boolean" }, { const: true }] } },
2622 { codes: makeCode(`Schema.Literal(true)`, `true`) }
2623 )
2624 })
2625
2626 it("preserves a matching literal before a boolean type", () => {
2627 assertFromJsonSchema(
2628 { schema: { allOf: [{ const: true }, { type: "boolean" }] } },
2629 { codes: makeCode(`Schema.Literal(true)`, `true`) }
2630 )
2631 })
2632
2633 it("combines a string with a reference", () => {
2634 const definition: JsonSchema.JsonSchema = { type: "string", minLength: 1 }
2635 assertFromJsonSchema(
2636 {
2637 schema: {
2638 type: "string",
2639 allOf: [{ $ref: "#/$defs/A" }],
2640 $defs: { A: definition }
2641 }
2642 },
2643 {
2644 codes: makeCode(
2645 `Schema.String.check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }))`,
2646 `string`
2647 )
2648 }
2649 )
2650 })
2651
2652 it("preserves annotations on a reference inside allOf", () => {
2653 const definition: JsonSchema.JsonSchema = { type: "string", minLength: 1 }
2654
2655 assertFromJsonSchema({
2656 schema: {
2657 type: "string",
2658 allOf: [{ $ref: "#/$defs/A", description: "annotated" }],
2659 $defs: { A: definition }
2660 }
2661 }, {
2662 codes: makeCode(
2663 `Schema.String.annotate({ "description": "annotated" }).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }))`,
2664 `string`
2665 )
2666 })
2667 })
2668
2669 it("preserves annotations on a root reference intersected with allOf", () => {
2670 const definition: JsonSchema.JsonSchema = { type: "string", minLength: 1 }
2671
2672 assertFromJsonSchema({
2673 schema: {
2674 $ref: "#/$defs/A",
2675 description: "annotated",
2676 allOf: [{ type: "string" }],
2677 $defs: { A: definition }
2678 }
2679 }, {
2680 codes: makeCode(
2681 `Schema.String.annotate({ "description": "annotated" }).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }))`,
2682 `string`
2683 )
2684 })
2685 })
2686
2687 it("preserves annotations through reference aliases", () => {
2688 assertFromJsonSchema({
2689 schema: {
2690 type: "string",
2691 allOf: [{ $ref: "#/$defs/A" }],
2692 $defs: {
2693 A: { $ref: "#/$defs/B", description: "alias" },
2694 B: { type: "string" }
2695 }
2696 }
2697 }, { codes: makeCode(`Schema.String.annotate({ "description": "alias" })`, `string`) })
2698 })
2699
2700 it("merges annotations on string intersections", () => {
2701 assertFromJsonSchema({
2702 schema: {
2703 allOf: [
2704 { type: "string", contentMediaType: "application/json" },
2705 { type: "string", contentSchema: { type: "number" } }
2706 ]
2707 }
2708 }, {
2709 codes: makeCode(
2710 `Schema.String.annotate({ "contentMediaType": "application/json", "contentSchema": { "type": "number" } })`,
2711 `string`
2712 )
2713 })
2714 })
2715
2716 it("merges constraints on overlapping required properties", () => {
2717 assertFromJsonSchema({
2718 schema: {
2719 type: "object",
2720 additionalProperties: false,
2721 properties: { a: { type: "string" } },
2722 required: ["a"],
2723 allOf: [{
2724 type: "object",
2725 additionalProperties: false,
2726 properties: { a: { type: "string", minLength: 2 } },
2727 required: ["a"]
2728 }]
2729 }
2730 }, {
2731 codes: makeCode(
2732 `Schema.Struct({ "a": Schema.String.check(Schema.isMinCodePoints(2).annotate({ "expected": "a string with at least 2 code points" })) })`,
2733 `{ readonly "a": string }`
2734 )
2735 })
2736 })
2737
2738 it("preserves optional properties when intersecting object fields", () => {
2739 assertFromJsonSchema({
2740 schema: {
2741 type: "object",
2742 additionalProperties: false,
2743 properties: { a: { type: "string" } },
2744 allOf: [{
2745 type: "object",
2746 additionalProperties: false,
2747 properties: { a: { minLength: 1 } }
2748 }]
2749 }
2750 }, {
2751 codes: makeCode(
2752 `Schema.Struct({ "a": Schema.optionalKey(Schema.String.check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }))) })`,
2753 `{ readonly "a"?: string }`
2754 )
2755 })
2756 })
2757
2758 it("keeps additionalProperties scopes separate", () => {
2759 for (
2760 const schema of [
2761 {
2762 type: "object",
2763 additionalProperties: false,
2764 allOf: [{ properties: { a: { type: "string" } } }]
2765 },
2766 {
2767 type: "object",
2768 properties: { a: { type: "string" } },
2769 allOf: [{ additionalProperties: false }]
2770 }
2771 ]
2772 ) {
2773 assertFromJsonSchema({ schema }, {
2774 codes: makeCode(`Schema.Struct({ "a": Schema.optionalKey(Schema.Never) })`, `{ readonly "a"?: never }`)
2775 })
2776 }
2777 })
2778
2779 it("does not move sibling properties into a closed scope", () => {
2780 assertFromJsonSchema({
2781 schema: {
2782 type: "object",
2783 properties: { a: { type: "string" } },
2784 additionalProperties: false,
2785 allOf: [{ properties: { b: { type: "number" } } }]
2786 }
2787 }, {
2788 codes: makeCode(
2789 `Schema.Struct({ "a": Schema.optionalKey(Schema.String), "b": Schema.optionalKey(Schema.Never) })`,
2790 `{ readonly "a"?: string, readonly "b"?: never }`
2791 )
2792 })
2793 })
2794
2795 it("rejects an object when a required sibling property is outside a closed scope", () => {
2796 assertFromJsonSchema({
2797 schema: {
2798 type: "object",
2799 additionalProperties: false,
2800 allOf: [{ properties: { a: { type: "string" } }, required: ["a"] }]
2801 }
2802 }, { codes: makeCode(`Schema.Never`, `never`) })
2803 })
2804
2805 it("keeps object keyword scopes through references", () => {
2806 assertFromJsonSchema({
2807 schema: {
2808 $ref: "#/$defs/Closed",
2809 allOf: [{ properties: { a: { type: "string" } } }],
2810 $defs: {
2811 Closed: { type: "object", additionalProperties: false }
2812 }
2813 }
2814 }, { codes: makeCode(`Schema.Struct({ "a": Schema.optionalKey(Schema.Never) })`, `{ readonly "a"?: never }`) })
2815 })
2816
2817 it("applies a sibling additionalProperties schema to fixed properties", () => {
2818 const schema = toSchemaFromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12({
2819 type: "object",
2820 properties: { a: { type: "string" } },
2821 allOf: [{ additionalProperties: { type: "boolean" } }]
2822 }))
2823 assertCode(schema, {
2824 codes: makeCode(
2825 `Schema.StructWithRest(Schema.Struct({ "a": Schema.optionalKey(Schema.Never) }), [Schema.Record(Schema.String, Schema.Boolean)])`,
2826 `{ readonly "a"?: never } & { readonly [x: string]: boolean }`
2827 )
2828 })
2829 deepStrictEqual(Schema.toJsonSchemaDocument(schema).schema, {
2830 type: "object",
2831 properties: { a: { not: {} } },
2832 allOf: [{ type: "object", additionalProperties: { type: "boolean" } }]
2833 })
2834 })
2835
2836 it("lowers open patterns over a finite object domain", () => {
2837 assertFromJsonSchema({
2838 schema: {
2839 type: "object",
2840 properties: { a: { type: "string" } },
2841 additionalProperties: false,
2842 allOf: [{ patternProperties: { "^a$": { minLength: 2 } } }]
2843 }
2844 }, {
2845 codes: makeCode(
2846 `Schema.Struct({ "a": Schema.optionalKey(Schema.String.check(Schema.isMinCodePoints(2).annotate({ "expected": "a string with at least 2 code points" }))) })`,
2847 `{ readonly "a"?: string }`
2848 )
2849 })
2850 })
2851
2852 it("rejects open pattern scopes", () => {
2853 for (const additional of [{}, { additionalProperties: true }, { additionalProperties: {} }] as const) {
2854 const schema = {
2855 type: "object",
2856 patternProperties: { "^a": { type: "number" } },
2857 ...additional
2858 } as const
2859 throws(
2860 () => toSchemaFromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12(schema)),
2861 `Cannot import open "patternProperties": unmatched keys cannot be typed correctly.\n at ["schema"]`
2862 )
2863 assertFromJsonSchema({ schema, options: { patterns: "ignore" } }, {
2864 codes: makeCode(
2865 `Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))`,
2866 `{ readonly [x: string]: Schema.Json }`
2867 )
2868 })
2869 }
2870 })
2871
2872 it("rejects open patterns with fixed properties", () => {
2873 throws(
2874 () =>
2875 toSchemaFromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12({
2876 type: "object",
2877 properties: { a: { type: "string" } },
2878 patternProperties: { "^a$": { minLength: 2 } }
2879 })),
2880 `Cannot import open "patternProperties": unmatched keys cannot be typed correctly.\n at ["schema"]`
2881 )
2882 })
2883
2884 it("rejects nested open pattern scopes with their source paths", () => {
2885 const open = { type: "object", patternProperties: { "^a": { type: "number" } } } as const
2886 const cases: ReadonlyArray<readonly [JsonSchema.JsonSchema, string]> = [
2887 [{ type: "array", items: open }, `["schema"]["items"]`],
2888 [{ type: "object", properties: { values: open } }, `["schema"]["properties"]["values"]`],
2889 [{ anyOf: [{ type: "string" }, open] }, `["schema"]["anyOf"][1]`],
2890 [{ $ref: "#/$defs/Values", $defs: { Values: open } }, `["definitions"]["Values"]`]
2891 ]
2892 for (const [schema, path] of cases) {
2893 throws(
2894 () => toSchemaFromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12(schema)),
2895 `Cannot import open "patternProperties": unmatched keys cannot be typed correctly.\n at ${path}`
2896 )
2897 }
2898 })
2899
2900 it("lowers an initially open pattern over a finite object domain", () => {
2901 assertFromJsonSchema({
2902 schema: {
2903 type: "object",
2904 patternProperties: { "^a$": { minLength: 2 } },
2905 allOf: [{
2906 properties: { a: { type: "string" } },
2907 additionalProperties: false
2908 }]
2909 }
2910 }, {
2911 codes: makeCode(
2912 `Schema.Struct({ "a": Schema.optionalKey(Schema.String.check(Schema.isMinCodePoints(2).annotate({ "expected": "a string with at least 2 code points" }))) })`,
2913 `{ readonly "a"?: string }`
2914 )
2915 })
2916 })
2917
2918 it("lowers a referenced open pattern over a finite object domain", () => {
2919 assertFromJsonSchema({
2920 schema: {
2921 type: "object",
2922 properties: { a: { type: "string" } },
2923 additionalProperties: false,
2924 allOf: [{ $ref: "#/$defs/Pattern" }],
2925 $defs: {
2926 Pattern: {
2927 type: "object",
2928 patternProperties: { "^a$": { minLength: 2 } }
2929 }
2930 }
2931 }
2932 }, {
2933 codes: makeCode(
2934 `Schema.Struct({ "a": Schema.optionalKey(Schema.String.check(Schema.isMinCodePoints(2).annotate({ "expected": "a string with at least 2 code points" }))) })`,
2935 `{ readonly "a"?: string }`
2936 )
2937 })
2938 })
2939
2940 it("rejects object scopes that require pattern complements", () => {
2941 throws(
2942 () =>
2943 toSchemaFromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12({
2944 type: "object",
2945 additionalProperties: false,
2946 patternProperties: { "^a": { type: "string" } },
2947 allOf: [
2948 {
2949 type: "object",
2950 additionalProperties: false,
2951 patternProperties: { "^b": { type: "number" } }
2952 }
2953 ]
2954 })),
2955 `Cannot import this closed patterned object: only one pattern without properties is supported.\n at ["schema"]["allOf"][0]`
2956 )
2957 })
2958
2959 it("preserves a closed pattern scope when intersecting a finite object domain", () => {
2960 assertFromJsonSchema({
2961 schema: {
2962 type: "object",
2963 properties: { a: { type: "number" }, b: { type: "number" } },
2964 additionalProperties: false,
2965 allOf: [{
2966 type: "object",
2967 patternProperties: { "^a": { type: "number" } },
2968 additionalProperties: false
2969 }]
2970 }
2971 }, {
2972 codes: makeCode(
2973 `Schema.Struct({ "a": Schema.optionalKey(Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" }))), "b": Schema.optionalKey(Schema.Never) })`,
2974 `{ readonly "a"?: number, readonly "b"?: never }`
2975 )
2976 })
2977 })
2978
2979 describe("type: object", () => {
2980 it("add properties", () => {
2981 assertFromJsonSchema(
2982 {
2983 schema: {
2984 type: "object",
2985 additionalProperties: false,
2986 allOf: [
2987 { properties: { a: { type: "string" } } }
2988 ]
2989 }
2990 },
2991 { codes: makeCode(`Schema.Struct({ "a": Schema.optionalKey(Schema.Never) })`, `{ readonly "a"?: never }`) }
2992 )
2993 })
2994
2995 it("add additionalProperties", () => {
2996 assertFromJsonSchema(
2997 {
2998 schema: {
2999 type: "object",
3000 allOf: [
3001 { additionalProperties: { type: "boolean" } }
3002 ]
3003 }
3004 },
3005 { codes: makeCode(`Schema.Record(Schema.String, Schema.Boolean)`, `{ readonly [x: string]: boolean }`) }
3006 )
3007 })
3008 })
3009 })
3010
3011 describe("unsupported validation keywords", () => {
3012 it("not", () => {
3013 throws(
3014 () =>
3015 toSchemaFromJsonSchemaDocument(JsonSchema.fromSchemaDraft07({
3016 type: "object",
3017 properties: {
3018 value: { not: { type: "string" } }
3019 }
3020 })),
3021 `Cannot import JSON Schema keyword "not": Effect has no equivalent constraint.\n at ["schema"]["properties"]["value"]["not"]`
3022 )
3023 })
3024
3025 for (
3026 const [keyword, value] of [
3027 ["$dynamicRef", "#node"],
3028 ["contains", { type: "string" }],
3029 ["dependentRequired", { a: ["b"] }],
3030 ["dependentSchemas", { a: { required: ["b"] } }],
3031 ["unevaluatedItems", false],
3032 ["unevaluatedProperties", false]
3033 ] as const
3034 ) {
3035 it(keyword, () => {
3036 throws(
3037 () => toSchemaFromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12({ [keyword]: value })),
3038 `Cannot import JSON Schema keyword "${keyword}": Effect has no equivalent constraint.\n at ["schema"][${
3039 JSON.stringify(keyword)
3040 }]`
3041 )
3042 })
3043 }
3044
3045 for (const branch of ["then", "else"] as const) {
3046 it(`if/${branch}`, () => {
3047 throws(
3048 () =>
3049 toSchemaFromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12({
3050 if: { type: "string" },
3051 [branch]: false
3052 })),
3053 `Cannot import JSON Schema keyword "if": Effect has no equivalent constraint.\n at ["schema"]["if"]`
3054 )
3055 })
3056 }
3057
3058 it("ignores inactive conditional and contains cardinality keywords", () => {
3059 for (
3060 const [keyword, value] of [
3061 ["if", false],
3062 ["then", false],
3063 ["else", false],
3064 ["minContains", 1],
3065 ["maxContains", 2]
3066 ] as const
3067 ) {
3068 assertFromJsonSchema(
3069 { schema: { [keyword]: value } },
3070 { codes: makeCode(`Schema.Json.annotate({ "expected": "JSON value" })`, `Schema.Json`) }
3071 )
3072 }
3073 })
3074
3075 it("ignores custom keywords", () => {
3076 assertFromJsonSchema({ schema: { custom: false } }, {
3077 codes: makeCode(`Schema.Json.annotate({ "expected": "JSON value" })`, `Schema.Json`)
3078 })
3079 })
3080 })
3081
3082 describe("options", () => {
3083 describe("patterns", () => {
3084 it("can reject previously exclusive branches when patterns are ignored", () => {
3085 const document = JsonSchema.fromSchemaDraft2020_12({
3086 oneOf: [
3087 { type: "string", pattern: "^a$" },
3088 { type: "string", pattern: "^b$" }
3089 ]
3090 })
3091 const applied = toSchemaFromJsonSchemaDocument(document, { patterns: "apply" })
3092 const ignored = toSchemaFromJsonSchemaDocument(document, { patterns: "ignore" })
3093 for (const input of ["a", "b"]) {
3094 assert.isTrue(Schema.is(applied)(input))
3095 assert.isFalse(Schema.is(ignored)(input))
3096 }
3097 })
3098
3099 it("rejects patterns by default", () => {
3100 for (
3101 const [schema, path] of [
3102 [{ type: "string", pattern: "^a+$" }, `["schema"]["pattern"]`],
3103 [
3104 { type: "object", patternProperties: { "^a+$": { type: "string" } } },
3105 `["schema"]["patternProperties"]["^a+$"]`
3106 ],
3107 [
3108 { type: "object", propertyNames: { pattern: "^a+$" } },
3109 `["schema"]["propertyNames"]["pattern"]`
3110 ]
3111 ] as const
3112 ) {
3113 throws(
3114 () => SchemaRepresentation.fromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12(schema)),
3115 `Patterns may block validation and are disabled. Use patterns: "apply" for trusted schemas or "ignore" to discard them.\n at ${path}`
3116 )
3117 }
3118 })
3119
3120 it("applies patterns explicitly", () => {
3121 assertFromJsonSchema({ schema: { type: "string", pattern: "^a+$" }, options: { patterns: "apply" } }, {
3122 codes: makeCode(
3123 `Schema.String.check(Schema.isPattern(new RegExp("^a+$", "u")).annotate({ "expected": "a string matching the RegExp ^a+$" }))`,
3124 `string`
3125 )
3126 })
3127 })
3128
3129 it("applies patterns whose source is canonicalized by RegExp", () => {
3130 for (
3131 const [pattern, source, matching, nonMatching] of [
3132 ["a/b", "a\\/b", "a/b", "ab"],
3133 ["", "(?:)", "anything", undefined],
3134 ["\n", "\\n", "a\nb", "ab"],
3135 ["\r", "\\r", "a\rb", "ab"],
3136 ["\u2028", "\\u2028", "a\u2028b", "ab"],
3137 ["\u2029", "\\u2029", "a\u2029b", "ab"]
3138 ] as const
3139 ) {
3140 const schema = toSchemaFromJsonSchemaDocument(
3141 JsonSchema.fromSchemaDraft2020_12({ type: "string", pattern })
3142 )
3143 const representation = SchemaRepresentation.toRepresentation(schema.ast).representation
3144 assert.strictEqual(representation._tag, "String")
3145 if (representation._tag === "String") {
3146 assert.deepStrictEqual(representation.checks[0]?.representation?.payload, { source, flags: "u" })
3147 }
3148 assert.isTrue(Schema.is(schema)(matching))
3149 if (nonMatching !== undefined) {
3150 assert.isFalse(Schema.is(schema)(nonMatching))
3151 }
3152 assert.isFalse(Schema.is(schema)(123))
3153 }
3154 })
3155
3156 it("ignores patterns explicitly", () => {
3157 assertFromJsonSchema({ schema: { type: "string", pattern: "^a+$" }, options: { patterns: "ignore" } }, {
3158 codes: makeCode(`Schema.String`, `string`)
3159 })
3160 assertFromJsonSchema({ schema: { type: "string", pattern: "[" }, options: { patterns: "ignore" } }, {
3161 codes: makeCode(`Schema.String`, `string`)
3162 })
3163 })
3164
3165 it("ignores pattern property value constraints explicitly", () => {
3166 assertFromJsonSchema({
3167 schema: {
3168 type: "object",
3169 patternProperties: {
3170 "^a+$": { type: "string" },
3171 "^b+$": { type: "number" }
3172 },
3173 additionalProperties: false
3174 },
3175 options: { patterns: "ignore" }
3176 }, {
3177 codes: makeCode(
3178 `Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))`,
3179 `{ readonly [x: string]: Schema.Json }`
3180 )
3181 })
3182 assertFromJsonSchema({
3183 schema: {
3184 type: "object",
3185 patternProperties: { "^a+$": { type: "string" } },
3186 additionalProperties: { type: "boolean" }
3187 },
3188 options: { patterns: "ignore" }
3189 }, {
3190 codes: makeCode(
3191 `Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))`,
3192 `{ readonly [x: string]: Schema.Json }`
3193 )
3194 })
3195 })
3196 })
3197
3198 describe("onEnter", () => {
3199 it("additionalProperties false via onEnter", () => {
3200 assertFromJsonSchema(
3201 {
3202 schema: {
3203 type: "object",
3204 properties: {
3205 a: { type: "string" }
3206 },
3207 required: ["a"]
3208 },
3209 options: {
3210 onEnter: (js) => {
3211 if (js.type === "object" && js.additionalProperties === undefined) {
3212 return { ...js, additionalProperties: false }
3213 }
3214 return js
3215 }
3216 }
3217 },
3218 { codes: makeCode(`Schema.Struct({ "a": Schema.String })`, `{ readonly "a": string }`) }
3219 )
3220 })
3221
3222 it("strips annotation keys via onEnter", () => {
3223 assertFromJsonSchema(
3224 {
3225 schema: {
3226 title: "a",
3227 description: "b",
3228 examples: ["d"]
3229 },
3230 options: {
3231 onEnter: (js) => {
3232 const out = { ...js }
3233 delete out.examples
3234 return out
3235 }
3236 }
3237 },
3238 {
3239 codes: makeCode(
3240 `Schema.Json.annotate({ "expected": "JSON value", "title": "a", "description": "b" })`,
3241 `Schema.Json`
3242 )
3243 }
3244 )
3245 })
3246
3247 it("filters annotations by predicate via onEnter", () => {
3248 assertFromJsonSchema(
3249 {
3250 schema: {
3251 title: "a",
3252 description: "b",
3253 examples: ["d"],
3254 default: "c"
3255 },
3256 options: {
3257 onEnter: (js) => {
3258 const out: any = {}
3259 for (const [k, v] of Object.entries(js)) {
3260 if (k === "title" || k === "default" || k === "type") out[k] = v
3261 }
3262 return out
3263 }
3264 }
3265 },
3266 {
3267 codes: makeCode(
3268 `Schema.Json.annotate({ "expected": "JSON value", "title": "a", "default": "c" })`,
3269 `Schema.Json`
3270 )
3271 }
3272 )
3273 })
3274
3275 it("default preserves all annotations", () => {
3276 assertFromJsonSchema(
3277 {
3278 schema: {
3279 title: "a",
3280 description: "b",
3281 default: "c",
3282 examples: ["d"]
3283 }
3284 },
3285 {
3286 codes: makeCode(
3287 `Schema.Json.annotate({ "expected": "JSON value", "title": "a", "description": "b", "default": "c", "examples": ["d"] })`,
3288 `Schema.Json`
3289 )
3290 }
3291 )
3292 })
3293 })
3294 })
3295})