1import { assert, describe, it } from "@effect/vitest"
2import { type JsonSchema, Schema } from "effect"
3import { toCodecOpenAI } from "effect/ai/OpenAiStructuredOutput"
4import * as Tool from "effect/ai/Tool"
5import { TestSchema } from "effect/testing"
6
7function assertJsonSchema(schema: Schema.Constraint, expected: JsonSchema.JsonSchema) {
8 if (expected.type === "object" && expected.anyOf === undefined) {
9 assert.deepStrictEqual(toCodecOpenAI(schema).jsonSchema, expected)
10 return
11 }
12 assert.deepStrictEqual(toCodecOpenAI(Schema.Struct({ value: schema })).jsonSchema, {
13 type: "object",
14 properties: { value: expected },
15 required: ["value"],
16 additionalProperties: false
17 })
18}
19
20function assertError(schema: Schema.Constraint, message: string) {
21 assert.throws(() => toCodecOpenAI(schema), message)
22}
23
24describe("toCodecOpenAI", () => {
25 describe("Canonical JSON and mechanical invariants", () => {
26 describe("Root", () => {
27 const message = `OpenAiStructuredOutput: Root JSON Schema must have type "object" and must not use "anyOf"`
28
29 it("rejects primitive roots", () => {
30 assertError(Schema.String, message)
31 })
32
33 it("rejects array roots", () => {
34 assertError(Schema.Array(Schema.String), message)
35 })
36
37 it("rejects anyOf roots", () => {
38 assertError(
39 Schema.Union([
40 Schema.Struct({ a: Schema.String }),
41 Schema.Struct({ b: Schema.String })
42 ]),
43 message
44 )
45 })
46
47 it("rejects unconstrained roots", () => {
48 assertError(Schema.Any, message)
49 assertError(Schema.Unknown, message)
50 assertError(Schema.Never, message)
51 })
52 })
53
54 it("encodes Undefined as null", () => {
55 assertJsonSchema(Schema.Undefined, { type: "null" })
56 })
57
58 it("encodes a bigint Literal as a string", () => {
59 assertJsonSchema(Schema.Literal(1n), { type: "string", enum: ["1"] })
60 })
61
62 describe("Arrays", () => {
63 it("encodes post-rest elements as object properties", async () => {
64 const schema = Schema.TupleWithRest(Schema.Tuple([]), [Schema.String, Schema.String])
65 const result = toCodecOpenAI(schema)
66 assert.deepStrictEqual(result.jsonSchema, {
67 type: "object",
68 properties: {
69 __rest__: { type: "array", items: { type: "string" } },
70 __tail_0__: { type: "string" }
71 },
72 required: ["__rest__", "__tail_0__"],
73 additionalProperties: false,
74 description:
75 "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements. Post-rest elements use '__tail_0__', '__tail_1__', and so on"
76 })
77 const asserts = new TestSchema.Asserts(result.codec)
78 await asserts.encoding().succeed(["a", "b"], { __rest__: ["a"], __tail_0__: "b" })
79 await asserts.decoding().succeed({ __rest__: ["a"], __tail_0__: "b" }, ["a", "b"])
80 })
81 })
82
83 describe("Objects", () => {
84 it("non-string property signature name", () => {
85 assertError(
86 Schema.Struct({ [Symbol.for("effect/Schema/test/a")]: Schema.String }),
87 "Objects property names must be strings"
88 )
89 })
90 })
91 })
92
93 describe("Suspend", () => {
94 it("no-transformation recursive schema", () => {
95 interface A {
96 readonly a: string
97 readonly as: ReadonlyArray<A>
98 }
99 const schema = Schema.Struct({
100 a: Schema.String.check(Schema.isStartingWith("a")),
101 as: Schema.Array(Schema.suspend((): Schema.Codec<A> => schema))
102 })
103 assertJsonSchema(schema, {
104 "type": "object",
105 "properties": {
106 "a": {
107 "type": "string",
108 "description": "a string starting with \"a\"",
109 "pattern": "^a"
110 },
111 "as": {
112 "type": "array",
113 "items": { "$ref": "#/$defs/Suspend_" }
114 }
115 },
116 "required": ["a", "as"],
117 "additionalProperties": false,
118 "$defs": {
119 "Suspend_": {
120 "type": "object",
121 "properties": {
122 "a": {
123 "type": "string",
124 "description": "a string starting with \"a\"",
125 "pattern": "^a"
126 },
127 "as": {
128 "type": "array",
129 "items": { "$ref": "#/$defs/Suspend_" }
130 }
131 },
132 "required": ["a", "as"],
133 "additionalProperties": false
134 }
135 }
136 })
137 })
138
139 it("transformation recursive schema", () => {
140 interface A {
141 readonly a: string
142 readonly as: readonly [A]
143 }
144 const schema = Schema.Struct({
145 a: Schema.String.check(Schema.isStartingWith("a")),
146 as: Schema.Tuple([Schema.suspend((): Schema.Codec<A> => schema)])
147 })
148 assertJsonSchema(schema, {
149 "type": "object",
150 "properties": {
151 "a": {
152 "type": "string",
153 "description": "a string starting with \"a\"",
154 "pattern": "^a"
155 },
156 "as": {
157 "type": "object",
158 "description":
159 "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements",
160 "properties": {
161 "0": {
162 "$ref": "#/$defs/Suspend_"
163 }
164 },
165 "required": ["0"],
166 "additionalProperties": false
167 }
168 },
169 "required": ["a", "as"],
170 "additionalProperties": false,
171 "$defs": {
172 "Suspend_": {
173 "type": "object",
174 "properties": {
175 "a": {
176 "type": "string",
177 "description": "a string starting with \"a\"",
178 "pattern": "^a"
179 },
180 "as": {
181 "type": "object",
182 "description":
183 "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements",
184 "properties": {
185 "0": {
186 "$ref": "#/$defs/Suspend_"
187 }
188 },
189 "required": ["0"],
190 "additionalProperties": false
191 }
192 },
193 "required": ["a", "as"],
194 "additionalProperties": false
195 }
196 }
197 })
198 })
199 })
200
201 it("Null", () => {
202 assertJsonSchema(Schema.Null, {
203 "type": "null"
204 })
205 })
206
207 describe("String", () => {
208 it("String", () => {
209 assertJsonSchema(Schema.String, {
210 "type": "string"
211 })
212 assertJsonSchema(Schema.String.annotate({ description: "description" }), {
213 "type": "string",
214 "description": "description"
215 })
216 })
217
218 it("String + supported format", () => {
219 assertJsonSchema(Schema.String.annotate({ format: "date-time" }), {
220 "type": "string",
221 "format": "date-time"
222 })
223 })
224
225 it("String + unsupported format", () => {
226 assertJsonSchema(Schema.String.annotate({ format: "int32" }), {
227 "type": "string",
228 "description": "a value with a format of int32"
229 })
230 })
231
232 it("String + isMinLength", () => {
233 assertJsonSchema(Schema.String.check(Schema.isMinLength(1)), {
234 "type": "string",
235 "description": "a value with a length of at least 1"
236 })
237 })
238
239 it("String + isMinLength + isMaxLength", () => {
240 assertJsonSchema(Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(2)), {
241 "type": "string",
242 "description": "a value with a length of at least 1 and a value with a length of at most 2"
243 })
244 })
245
246 it("String + startsWith", () => {
247 assertJsonSchema(Schema.String.check(Schema.isStartingWith("a")), {
248 "type": "string",
249 "description": `a string starting with "a"`,
250 "pattern": "^a"
251 })
252 })
253
254 it("String + startsWith + endsWith", () => {
255 assertJsonSchema(Schema.String.check(Schema.isStartingWith("a"), Schema.isEndingWith("b")), {
256 "type": "string",
257 "description": `a string starting with "a" and a string ending with "b"`,
258 "pattern": "^(?=[\\s\\S]*?(?:^a))(?=[\\s\\S]*?(?:b$))"
259 })
260 })
261
262 it("String + pattern", () => {
263 assertJsonSchema(Schema.String.check(Schema.isPattern(/^a/u)), {
264 "type": "string",
265 "description": `a string matching the RegExp ^a`,
266 "pattern": "^a"
267 })
268 })
269 })
270
271 describe("Number", () => {
272 it("Number", () => {
273 assertJsonSchema(Schema.Number, {
274 "anyOf": [
275 { "type": "number" },
276 { "type": "string", "enum": ["Infinity", "-Infinity", "NaN"] }
277 ]
278 })
279 })
280
281 describe("Finite", () => {
282 it("Finite", () => {
283 assertJsonSchema(Schema.Finite, {
284 "type": "number"
285 })
286 assertJsonSchema(Schema.Finite.annotate({ description: "description" }), {
287 "type": "number",
288 "description": "description"
289 })
290 })
291
292 it("Finite + string format", () => {
293 assertJsonSchema(Schema.Finite.annotate({ format: "duration" }), {
294 "type": "number",
295 "description": "a value with a format of duration"
296 })
297 })
298
299 it("Finite + unsupported format", () => {
300 assertJsonSchema(Schema.Finite.annotate({ format: "int32" }), {
301 "type": "number",
302 "description": "a value with a format of int32"
303 })
304 })
305
306 it("Finite + isGreaterThan", () => {
307 assertJsonSchema(Schema.Finite.check(Schema.isGreaterThan(1)), {
308 "type": "number",
309 "description": "a value greater than 1",
310 "exclusiveMinimum": 1
311 })
312 })
313
314 it("Finite + isGreaterThanOrEqualTo", () => {
315 assertJsonSchema(Schema.Finite.check(Schema.isGreaterThanOrEqualTo(1)), {
316 "type": "number",
317 "description": "a value greater than or equal to 1",
318 "minimum": 1
319 })
320 })
321
322 it("Finite + isLessThan", () => {
323 assertJsonSchema(Schema.Finite.check(Schema.isLessThan(2)), {
324 "type": "number",
325 "description": "a value less than 2",
326 "exclusiveMaximum": 2
327 })
328 })
329
330 it("Finite + isLessThanOrEqualTo", () => {
331 assertJsonSchema(Schema.Finite.check(Schema.isLessThanOrEqualTo(2)), {
332 "type": "number",
333 "description": "a value less than or equal to 2",
334 "maximum": 2
335 })
336 })
337
338 it("Finite + isBetween", () => {
339 assertJsonSchema(Schema.Finite.check(Schema.isBetween({ minimum: 1, maximum: 2 })), {
340 "type": "number",
341 "description": "a value between 1 and 2",
342 "minimum": 1,
343 "maximum": 2
344 })
345 })
346
347 it("Finite + isMultipleOf", () => {
348 assertJsonSchema(Schema.Finite.check(Schema.isMultipleOf(2)), {
349 "type": "number",
350 "description": "a value that is a multiple of 2",
351 "multipleOf": 2
352 })
353 })
354
355 it("Finite + isGreaterThan + isLessThan", () => {
356 assertJsonSchema(Schema.Finite.check(Schema.isGreaterThan(1), Schema.isLessThan(2)), {
357 "type": "number",
358 "description": "a value greater than 1 and a value less than 2",
359 "exclusiveMinimum": 1,
360 "exclusiveMaximum": 2
361 })
362 })
363 })
364
365 describe("Int", () => {
366 it("Int", () => {
367 assertJsonSchema(Schema.Int, {
368 "type": "integer"
369 })
370 assertJsonSchema(Schema.Int.annotate({ description: "description" }), {
371 "type": "integer",
372 "description": "description"
373 })
374 })
375
376 it("Int + string format", () => {
377 assertJsonSchema(Schema.Int.annotate({ format: "duration" }), {
378 "type": "integer",
379 "description": "a value with a format of duration"
380 })
381 })
382
383 it("Int + unsupported format", () => {
384 assertJsonSchema(Schema.Int.annotate({ format: "int32" }), {
385 "type": "integer",
386 "description": "a value with a format of int32"
387 })
388 })
389
390 it("Int + isGreaterThan", () => {
391 assertJsonSchema(Schema.Int.check(Schema.isGreaterThan(1)), {
392 "type": "integer",
393 "description": "a value greater than 1",
394 "exclusiveMinimum": 1
395 })
396 })
397
398 it("Int + isGreaterThan + isLessThan", () => {
399 assertJsonSchema(Schema.Int.check(Schema.isGreaterThan(1), Schema.isLessThan(2)), {
400 "type": "integer",
401 "description": "a value greater than 1 and a value less than 2",
402 "exclusiveMinimum": 1,
403 "exclusiveMaximum": 2
404 })
405 })
406 })
407 })
408
409 describe("Literal", () => {
410 it("Literal(string)", () => {
411 assertJsonSchema(Schema.Literal("a"), {
412 "type": "string",
413 "enum": ["a"]
414 })
415 })
416
417 it("Literal(number)", () => {
418 assertJsonSchema(Schema.Literal(1), {
419 "type": "number",
420 "enum": [1]
421 })
422 })
423
424 it("Literal(boolean)", () => {
425 assertJsonSchema(Schema.Literal(true), {
426 "type": "boolean",
427 "enum": [true]
428 })
429 })
430 })
431
432 describe("Union", () => {
433 it("oneOf", () => {
434 assertJsonSchema(Schema.Union([Schema.String, Schema.Finite], { mode: "oneOf" }), {
435 "anyOf": [
436 { "type": "string" },
437 { "type": "number" }
438 ]
439 })
440 })
441
442 it("Union(NonEmptyString, Int)", () => {
443 assertJsonSchema(Schema.Union([Schema.NonEmptyString, Schema.Int]), {
444 "anyOf": [
445 { "type": "string", "description": "a value with a length of at least 1" },
446 { "type": "integer" }
447 ]
448 })
449 })
450 })
451
452 describe("Array", () => {
453 it("Array(String)", () => {
454 assertJsonSchema(Schema.Array(Schema.String), {
455 "type": "array",
456 "items": { "type": "string" }
457 })
458 assertJsonSchema(Schema.Array(Schema.String).annotate({ description: "description" }), {
459 "type": "array",
460 "items": { "type": "string" },
461 "description": "description"
462 })
463 })
464
465 it("Array(String) + isMinLength", () => {
466 assertJsonSchema(Schema.Array(Schema.String).check(Schema.isMinLength(1)), {
467 "type": "array",
468 "items": { "type": "string" },
469 "description": "a value with a length of at least 1",
470 "minItems": 1
471 })
472 })
473
474 it("Array(String) + isMinLength + isMaxLength", () => {
475 assertJsonSchema(Schema.Array(Schema.String).check(Schema.isMinLength(1), Schema.isMaxLength(2)), {
476 "type": "array",
477 "items": { "type": "string" },
478 "description": "a value with a length of at least 1 and a value with a length of at most 2",
479 "minItems": 1,
480 "maxItems": 2
481 })
482 })
483 })
484
485 describe("Tuple", () => {
486 it("Tuple([])", () => {
487 assertJsonSchema(Schema.Tuple([]), {
488 "type": "array",
489 "items": false
490 })
491 })
492
493 it("Tuple([String, Finite])", async () => {
494 const schema = Schema.Tuple([Schema.String, Schema.Finite])
495 assertJsonSchema(schema, {
496 "type": "object",
497 "description":
498 "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements",
499 "properties": {
500 "0": { "type": "string" },
501 "1": { "type": "number" }
502 },
503 "required": ["0", "1"],
504 "additionalProperties": false
505 })
506
507 const codec = toCodecOpenAI(schema).codec
508 const asserts = new TestSchema.Asserts(codec)
509
510 const encoding = asserts.encoding()
511 await encoding.succeed(["a", 1], { "0": "a", "1": 1 })
512
513 const decoding = asserts.decoding()
514 await decoding.succeed({ "0": "a", "1": 1 }, ["a", 1])
515 })
516
517 it("Tuple([String, Finite]) + description", () => {
518 const schema = Schema.Tuple([Schema.String, Schema.Finite]).annotate({ description: "description" })
519 assertJsonSchema(schema, {
520 "type": "object",
521 "description":
522 "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements; description",
523 "properties": {
524 "0": { "type": "string" },
525 "1": { "type": "number" }
526 },
527 "required": ["0", "1"],
528 "additionalProperties": false
529 })
530 })
531
532 it("Tuple([String, Finite?])", async () => {
533 const schema = Schema.Tuple([Schema.String, Schema.optionalKey(Schema.Finite)])
534 assertJsonSchema(schema, {
535 "type": "object",
536 "description":
537 "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements",
538 "properties": {
539 "0": { "type": "string" },
540 "1": {
541 "anyOf": [
542 { "type": "number" },
543 { "type": "null" }
544 ]
545 }
546 },
547 "required": ["0", "1"],
548 "additionalProperties": false
549 })
550
551 const codec = toCodecOpenAI(schema).codec
552 const asserts = new TestSchema.Asserts(codec)
553
554 const encoding = asserts.encoding()
555 await encoding.succeed(["a", 1], { "0": "a", "1": 1 })
556 await encoding.succeed(["a"], { "0": "a", "1": null })
557
558 const decoding = asserts.decoding()
559 await decoding.succeed({ "0": "a", "1": 1 }, ["a", 1])
560 await decoding.succeed({ "0": "a", "1": null }, ["a"])
561 })
562 })
563
564 describe("TupleWithRest", () => {
565 it("TupleWithRest([String, Finite], Boolean)", async () => {
566 const schema = Schema.TupleWithRest(
567 Schema.Tuple([Schema.String, Schema.Finite]),
568 [Schema.Boolean]
569 )
570 assertJsonSchema(schema, {
571 "type": "object",
572 "description":
573 "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements",
574 "properties": {
575 "0": { "type": "string" },
576 "1": { "type": "number" },
577 "__rest__": { "type": "array", "items": { "type": "boolean" } }
578 },
579 "required": ["0", "1", "__rest__"],
580 "additionalProperties": false
581 })
582 const codec = toCodecOpenAI(schema).codec
583 const asserts = new TestSchema.Asserts(codec)
584
585 const encoding = asserts.encoding()
586 await encoding.succeed(["a", 1], { "0": "a", "1": 1, "__rest__": [] })
587 await encoding.succeed(["a", 1, true], { "0": "a", "1": 1, "__rest__": [true] })
588 await encoding.succeed(["a", 1, true, false], { "0": "a", "1": 1, "__rest__": [true, false] })
589
590 const decoding = asserts.decoding()
591 await decoding.succeed({ "0": "a", "1": 1, "__rest__": [] }, ["a", 1])
592 await decoding.succeed({ "0": "a", "1": 1, "__rest__": [true] }, ["a", 1, true])
593 await decoding.succeed({ "0": "a", "1": 1, "__rest__": [true, false] }, ["a", 1, true, false])
594 })
595 })
596
597 describe("Struct", () => {
598 it("required properties", () => {
599 assertJsonSchema(Schema.Struct({ a: Schema.String }), {
600 "type": "object",
601 "properties": {
602 "a": { "type": "string" }
603 },
604 "required": ["a"],
605 "additionalProperties": false
606 })
607 })
608
609 it("optional properties", async () => {
610 const schema = Schema.Struct({
611 a: Schema.String,
612 b: Schema.optionalKey(Schema.Finite)
613 })
614 assertJsonSchema(
615 schema,
616 {
617 "type": "object",
618 "properties": {
619 "a": { "type": "string" },
620 "b": {
621 "anyOf": [
622 { "type": "number" },
623 { "type": "null" }
624 ]
625 }
626 },
627 "required": ["a", "b"],
628 "additionalProperties": false
629 }
630 )
631 const codec = toCodecOpenAI(schema).codec
632 const asserts = new TestSchema.Asserts(codec)
633
634 const encoding = asserts.encoding()
635 await encoding.succeed({ a: "a", b: 1 })
636 await encoding.succeed({ a: "a" }, { a: "a", b: null })
637
638 const decoding = asserts.decoding()
639 await decoding.succeed({ "a": "a", "b": 1 })
640 await decoding.succeed({ "a": "a", "b": null }, { a: "a" })
641 })
642 })
643
644 it("Class", () => {
645 class Person extends Schema.Class<Person>("Person")({
646 name: Schema.String
647 }) {}
648
649 assertJsonSchema(Person, {
650 "type": "object",
651 "properties": {
652 "name": { "type": "string" }
653 },
654 "required": ["name"],
655 "additionalProperties": false,
656 "$defs": {
657 "PersonEncoded": {
658 "type": "object",
659 "properties": {
660 "name": { "type": "string" }
661 },
662 "required": ["name"],
663 "additionalProperties": false
664 }
665 }
666 })
667 })
668
669 describe("Record", () => {
670 it("EmptyParams", () => {
671 assertJsonSchema(Tool.EmptyParams, {
672 "type": "object",
673 "properties": {},
674 "additionalProperties": false
675 })
676 assertJsonSchema(Schema.Record(Schema.String, Schema.Never), {
677 "type": "object",
678 "properties": {},
679 "additionalProperties": false
680 })
681 })
682
683 it("Record(String, Finite)", async () => {
684 const schema = Schema.Record(Schema.String, Schema.Finite)
685 assertJsonSchema(schema, {
686 "type": "array",
687 "description": "Object encoded as array of [key, value] pairs. Apply object constraints to the decoded object",
688 "items": {
689 "type": "object",
690 "description":
691 "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements",
692 "properties": {
693 "0": { "type": "string" },
694 "1": { "type": "number" }
695 },
696 "required": ["0", "1"],
697 "additionalProperties": false
698 }
699 })
700 const codec = toCodecOpenAI(Schema.Struct({ value: schema })).codec
701 const asserts = new TestSchema.Asserts(codec)
702
703 const encoding = asserts.encoding()
704 await encoding.succeed(
705 { value: { "a": 1, "b": 2 } },
706 { value: [{ 0: "a", 1: 1 }, { 0: "b", 1: 2 }] }
707 )
708
709 const decoding = asserts.decoding()
710 await decoding.succeed(
711 { value: [{ 0: "a", 1: 1 }, { 0: "b", 1: 2 }] },
712 { value: { "a": 1, "b": 2 } }
713 )
714 })
715
716 it("Record with properties and an index signature", async () => {
717 const schema = Schema.Record(
718 Schema.Union([Schema.Literal("fixed"), Schema.String]),
719 Schema.String
720 )
721 const expected = {
722 type: "array",
723 description: "Object encoded as array of [key, value] pairs. Apply object constraints to the decoded object",
724 items: {
725 type: "object",
726 description:
727 "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements",
728 properties: {
729 "0": {
730 anyOf: [
731 { type: "string", enum: ["fixed"] },
732 { type: "string" }
733 ]
734 },
735 "1": { type: "string" }
736 },
737 required: ["0", "1"],
738 additionalProperties: false
739 }
740 } as const
741 assertJsonSchema(schema, expected)
742
743 const result = toCodecOpenAI(Schema.Struct({ value: schema }))
744
745 const asserts = new TestSchema.Asserts(result.codec)
746 await asserts.encoding().succeed(
747 { value: { fixed: "required", dynamic: "value" } },
748 { value: [{ 0: "fixed", 1: "required" }, { 0: "dynamic", 1: "value" }] }
749 )
750 await asserts.decoding().succeed(
751 { value: [{ 0: "fixed", 1: "required" }, { 0: "dynamic", 1: "value" }] },
752 { value: { fixed: "required", dynamic: "value" } }
753 )
754 assert.strictEqual(
755 Schema.decodeUnknownExit(result.codec)({ value: [{ 0: "dynamic", 1: "value" }] })._tag,
756 "Failure"
757 )
758 })
759
760 it("Record(String, Finite) + description", () => {
761 const schema = Schema.Record(Schema.String, Schema.Finite).annotate({ description: "description" })
762 assertJsonSchema(schema, {
763 "type": "array",
764 "description":
765 "Object encoded as array of [key, value] pairs. Apply object constraints to the decoded object; description",
766 "items": {
767 "type": "object",
768 "description":
769 "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements",
770 "properties": {
771 "0": { "type": "string" },
772 "1": { "type": "number" }
773 },
774 "required": ["0", "1"],
775 "additionalProperties": false
776 }
777 })
778 })
779
780 it("Record(String, Finite) + isMinProperties", async () => {
781 const schema = Schema.Record(Schema.String, Schema.Finite).check(Schema.isMinProperties(2))
782 assertJsonSchema(schema, {
783 "type": "array",
784 "description":
785 "Object encoded as array of [key, value] pairs. Apply object constraints to the decoded object; a value with at least 2 entries",
786 "items": {
787 "type": "object",
788 "description":
789 "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements",
790 "properties": {
791 "0": { "type": "string" },
792 "1": { "type": "number" }
793 },
794 "required": ["0", "1"],
795 "additionalProperties": false
796 },
797 "minItems": 2
798 })
799 const result = toCodecOpenAI(Schema.Struct({ value: schema }))
800 await new TestSchema.Asserts(result.codec).decoding().fail(
801 { value: [{ 0: "a", 1: 1 }, { 0: "a", 1: 2 }] },
802 `Expected a value with at least 2 entries
803 at ["value"]`
804 )
805 })
806
807 it("Record(String, Finite) + isMinProperties + description", () => {
808 const schema = Schema.Record(Schema.String, Schema.Finite).check(
809 Schema.isMinProperties(2, { description: "description" })
810 )
811 assertJsonSchema(schema, {
812 "type": "array",
813 "description":
814 "Object encoded as array of [key, value] pairs. Apply object constraints to the decoded object; description",
815 "items": {
816 "type": "object",
817 "description":
818 "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements",
819 "properties": {
820 "0": { "type": "string" },
821 "1": { "type": "number" }
822 },
823 "required": ["0", "1"],
824 "additionalProperties": false
825 },
826 "minItems": 2
827 })
828 })
829 })
830})