zod 4.1.2 → 4.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/v4/classic/tests/to-json-schema.test.ts +133 -9
- package/src/v4/core/to-json-schema.ts +17 -7
- package/src/v4/core/versions.ts +1 -1
- package/v4/core/to-json-schema.cjs +14 -7
- package/v4/core/to-json-schema.js +14 -7
- package/v4/core/versions.cjs +1 -1
- package/v4/core/versions.js +1 -1
package/package.json
CHANGED
|
@@ -571,6 +571,19 @@ describe("toJSONSchema", () => {
|
|
|
571
571
|
`);
|
|
572
572
|
});
|
|
573
573
|
|
|
574
|
+
test("number with exclusive min-max openapi", () => {
|
|
575
|
+
const schema = z.number().lt(100).gt(1);
|
|
576
|
+
expect(z.toJSONSchema(schema, { target: "openapi-3.0" })).toMatchInlineSnapshot(`
|
|
577
|
+
{
|
|
578
|
+
"exclusiveMaximum": true,
|
|
579
|
+
"exclusiveMinimum": true,
|
|
580
|
+
"maximum": 100,
|
|
581
|
+
"minimum": 1,
|
|
582
|
+
"type": "number",
|
|
583
|
+
}
|
|
584
|
+
`);
|
|
585
|
+
});
|
|
586
|
+
|
|
574
587
|
test("arrays", () => {
|
|
575
588
|
expect(z.toJSONSchema(z.array(z.string()))).toMatchInlineSnapshot(`
|
|
576
589
|
{
|
|
@@ -652,6 +665,18 @@ describe("toJSONSchema", () => {
|
|
|
652
665
|
`);
|
|
653
666
|
});
|
|
654
667
|
|
|
668
|
+
test("record openapi", () => {
|
|
669
|
+
const schema = z.record(z.string(), z.boolean());
|
|
670
|
+
expect(z.toJSONSchema(schema, { target: "openapi-3.0" })).toMatchInlineSnapshot(`
|
|
671
|
+
{
|
|
672
|
+
"additionalProperties": {
|
|
673
|
+
"type": "boolean",
|
|
674
|
+
},
|
|
675
|
+
"type": "object",
|
|
676
|
+
}
|
|
677
|
+
`);
|
|
678
|
+
});
|
|
679
|
+
|
|
655
680
|
test("tuple", () => {
|
|
656
681
|
const schema = z.tuple([z.string(), z.number()]);
|
|
657
682
|
expect(z.toJSONSchema(schema)).toMatchInlineSnapshot(`
|
|
@@ -695,6 +720,51 @@ describe("toJSONSchema", () => {
|
|
|
695
720
|
const schema = z.tuple([z.string(), z.number()]);
|
|
696
721
|
expect(z.toJSONSchema(schema, { target: "openapi-3.0" })).toMatchInlineSnapshot(`
|
|
697
722
|
{
|
|
723
|
+
"items": {
|
|
724
|
+
"anyOf": [
|
|
725
|
+
{
|
|
726
|
+
"type": "string",
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
"type": "number",
|
|
730
|
+
},
|
|
731
|
+
],
|
|
732
|
+
},
|
|
733
|
+
"maxItems": 2,
|
|
734
|
+
"minItems": 2,
|
|
735
|
+
"type": "array",
|
|
736
|
+
}
|
|
737
|
+
`);
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
test("tuple with rest openapi", () => {
|
|
741
|
+
const schema = z.tuple([z.string(), z.number()]).rest(z.boolean());
|
|
742
|
+
expect(z.toJSONSchema(schema, { target: "openapi-3.0" })).toMatchInlineSnapshot(`
|
|
743
|
+
{
|
|
744
|
+
"items": {
|
|
745
|
+
"anyOf": [
|
|
746
|
+
{
|
|
747
|
+
"type": "string",
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
"type": "number",
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
"type": "boolean",
|
|
754
|
+
},
|
|
755
|
+
],
|
|
756
|
+
},
|
|
757
|
+
"minItems": 2,
|
|
758
|
+
"type": "array",
|
|
759
|
+
}
|
|
760
|
+
`);
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
test("tuple draft-7", () => {
|
|
764
|
+
const schema = z.tuple([z.string(), z.number()]);
|
|
765
|
+
expect(z.toJSONSchema(schema, { target: "draft-7", io: "input" })).toMatchInlineSnapshot(`
|
|
766
|
+
{
|
|
767
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
698
768
|
"items": [
|
|
699
769
|
{
|
|
700
770
|
"type": "string",
|
|
@@ -703,17 +773,19 @@ describe("toJSONSchema", () => {
|
|
|
703
773
|
"type": "number",
|
|
704
774
|
},
|
|
705
775
|
],
|
|
706
|
-
"maxItems": 2,
|
|
707
|
-
"minItems": 2,
|
|
708
776
|
"type": "array",
|
|
709
777
|
}
|
|
710
778
|
`);
|
|
711
779
|
});
|
|
712
780
|
|
|
713
|
-
test("tuple with rest
|
|
781
|
+
test("tuple with rest draft-7", () => {
|
|
714
782
|
const schema = z.tuple([z.string(), z.number()]).rest(z.boolean());
|
|
715
|
-
expect(z.toJSONSchema(schema, { target: "
|
|
783
|
+
expect(z.toJSONSchema(schema, { target: "draft-7", io: "input" })).toMatchInlineSnapshot(`
|
|
716
784
|
{
|
|
785
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
786
|
+
"additionalItems": {
|
|
787
|
+
"type": "boolean",
|
|
788
|
+
},
|
|
717
789
|
"items": [
|
|
718
790
|
{
|
|
719
791
|
"type": "string",
|
|
@@ -721,14 +793,60 @@ describe("toJSONSchema", () => {
|
|
|
721
793
|
{
|
|
722
794
|
"type": "number",
|
|
723
795
|
},
|
|
796
|
+
],
|
|
797
|
+
"type": "array",
|
|
798
|
+
}
|
|
799
|
+
`);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
test("tuple with rest draft-7 - issue #5151 regression test", () => {
|
|
803
|
+
// This test addresses issue #5151: tuple with rest elements and ids
|
|
804
|
+
// in draft-7 had incorrect internal path handling affecting complex scenarios
|
|
805
|
+
const primarySchema = z.string().meta({ id: "primary" });
|
|
806
|
+
const restSchema = z.number().meta({ id: "rest" });
|
|
807
|
+
const testSchema = z.tuple([primarySchema], restSchema);
|
|
808
|
+
|
|
809
|
+
// Test both final output structure AND internal path handling
|
|
810
|
+
const capturedPaths: string[] = [];
|
|
811
|
+
const result = z.toJSONSchema(testSchema, {
|
|
812
|
+
target: "draft-7",
|
|
813
|
+
override: (ctx) => capturedPaths.push(ctx.path.join("/")),
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
// Verify correct draft-7 structure with metadata extraction
|
|
817
|
+
expect(result).toMatchInlineSnapshot(`
|
|
818
|
+
{
|
|
819
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
820
|
+
"additionalItems": {
|
|
821
|
+
"$ref": "#/definitions/rest",
|
|
822
|
+
},
|
|
823
|
+
"definitions": {
|
|
824
|
+
"primary": {
|
|
825
|
+
"id": "primary",
|
|
826
|
+
"type": "string",
|
|
827
|
+
},
|
|
828
|
+
"rest": {
|
|
829
|
+
"id": "rest",
|
|
830
|
+
"type": "number",
|
|
831
|
+
},
|
|
832
|
+
},
|
|
833
|
+
"items": [
|
|
724
834
|
{
|
|
725
|
-
"
|
|
835
|
+
"$ref": "#/definitions/primary",
|
|
726
836
|
},
|
|
727
837
|
],
|
|
728
|
-
"minItems": 2,
|
|
729
838
|
"type": "array",
|
|
730
839
|
}
|
|
731
840
|
`);
|
|
841
|
+
|
|
842
|
+
// Verify internal paths are correct (this was the actual bug)
|
|
843
|
+
expect(capturedPaths).toContain("items/0"); // prefix items should use "items" path
|
|
844
|
+
expect(capturedPaths).toContain("additionalItems"); // rest should use "additionalItems" path
|
|
845
|
+
expect(capturedPaths).not.toContain("prefixItems/0"); // should not use draft-2020-12 paths
|
|
846
|
+
|
|
847
|
+
// Structural validations
|
|
848
|
+
expect(Array.isArray(result.items)).toBe(true);
|
|
849
|
+
expect(result.additionalItems).toBeDefined();
|
|
732
850
|
});
|
|
733
851
|
|
|
734
852
|
test("promise", () => {
|
|
@@ -1554,7 +1672,9 @@ test("unrepresentable literal values are ignored", () => {
|
|
|
1554
1672
|
}
|
|
1555
1673
|
`);
|
|
1556
1674
|
|
|
1557
|
-
const b = z.z.toJSONSchema(z.literal([undefined, null, 5, BigInt(1324)]), {
|
|
1675
|
+
const b = z.z.toJSONSchema(z.literal([undefined, null, 5, BigInt(1324)]), {
|
|
1676
|
+
unrepresentable: "any",
|
|
1677
|
+
});
|
|
1558
1678
|
expect(b).toMatchInlineSnapshot(`
|
|
1559
1679
|
{
|
|
1560
1680
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
@@ -1566,7 +1686,9 @@ test("unrepresentable literal values are ignored", () => {
|
|
|
1566
1686
|
}
|
|
1567
1687
|
`);
|
|
1568
1688
|
|
|
1569
|
-
const c = z.z.toJSONSchema(z.literal([undefined]), {
|
|
1689
|
+
const c = z.z.toJSONSchema(z.literal([undefined]), {
|
|
1690
|
+
unrepresentable: "any",
|
|
1691
|
+
});
|
|
1570
1692
|
expect(c).toMatchInlineSnapshot(`
|
|
1571
1693
|
{
|
|
1572
1694
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
@@ -1849,7 +1971,9 @@ test("basic registry", () => {
|
|
|
1849
1971
|
myRegistry.add(User, { id: "User" });
|
|
1850
1972
|
myRegistry.add(Post, { id: "Post" });
|
|
1851
1973
|
|
|
1852
|
-
const result = z.z.toJSONSchema(myRegistry, {
|
|
1974
|
+
const result = z.z.toJSONSchema(myRegistry, {
|
|
1975
|
+
uri: (id) => `https://example.com/${id}.json`,
|
|
1976
|
+
});
|
|
1853
1977
|
expect(result).toMatchInlineSnapshot(`
|
|
1854
1978
|
{
|
|
1855
1979
|
"schemas": {
|
|
@@ -183,7 +183,7 @@ export class JSONSchemaGenerator {
|
|
|
183
183
|
else json.type = "number";
|
|
184
184
|
|
|
185
185
|
if (typeof exclusiveMinimum === "number") {
|
|
186
|
-
if (this.target === "draft-4") {
|
|
186
|
+
if (this.target === "draft-4" || this.target === "openapi-3.0") {
|
|
187
187
|
json.minimum = exclusiveMinimum;
|
|
188
188
|
json.exclusiveMinimum = true;
|
|
189
189
|
} else {
|
|
@@ -199,7 +199,7 @@ export class JSONSchemaGenerator {
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
if (typeof exclusiveMaximum === "number") {
|
|
202
|
-
if (this.target === "draft-4") {
|
|
202
|
+
if (this.target === "draft-4" || this.target === "openapi-3.0") {
|
|
203
203
|
json.maximum = exclusiveMaximum;
|
|
204
204
|
json.exclusiveMaximum = true;
|
|
205
205
|
} else {
|
|
@@ -368,13 +368,21 @@ export class JSONSchemaGenerator {
|
|
|
368
368
|
case "tuple": {
|
|
369
369
|
const json: JSONSchema.ArraySchema = _json as any;
|
|
370
370
|
json.type = "array";
|
|
371
|
+
|
|
372
|
+
const prefixPath = this.target === "draft-2020-12" ? "prefixItems" : "items";
|
|
373
|
+
const restPath =
|
|
374
|
+
this.target === "draft-2020-12" ? "items" : this.target === "openapi-3.0" ? "items" : "additionalItems";
|
|
375
|
+
|
|
371
376
|
const prefixItems = def.items.map((x, i) =>
|
|
372
|
-
this.process(x, {
|
|
377
|
+
this.process(x, {
|
|
378
|
+
...params,
|
|
379
|
+
path: [...params.path, prefixPath, i],
|
|
380
|
+
})
|
|
373
381
|
);
|
|
374
382
|
const rest = def.rest
|
|
375
383
|
? this.process(def.rest, {
|
|
376
384
|
...params,
|
|
377
|
-
path: [...params.path, "items
|
|
385
|
+
path: [...params.path, restPath, ...(this.target === "openapi-3.0" ? [def.items.length] : [])],
|
|
378
386
|
})
|
|
379
387
|
: null;
|
|
380
388
|
|
|
@@ -384,9 +392,11 @@ export class JSONSchemaGenerator {
|
|
|
384
392
|
json.items = rest;
|
|
385
393
|
}
|
|
386
394
|
} else if (this.target === "openapi-3.0") {
|
|
387
|
-
json.items =
|
|
395
|
+
json.items = {
|
|
396
|
+
anyOf: [...prefixItems],
|
|
397
|
+
};
|
|
388
398
|
if (rest) {
|
|
389
|
-
json.items.push(rest);
|
|
399
|
+
json.items.anyOf!.push(rest);
|
|
390
400
|
}
|
|
391
401
|
json.minItems = prefixItems.length;
|
|
392
402
|
if (!rest) {
|
|
@@ -411,7 +421,7 @@ export class JSONSchemaGenerator {
|
|
|
411
421
|
case "record": {
|
|
412
422
|
const json: JSONSchema.ObjectSchema = _json as any;
|
|
413
423
|
json.type = "object";
|
|
414
|
-
if (this.target
|
|
424
|
+
if (this.target === "draft-7" || this.target === "draft-2020-12") {
|
|
415
425
|
json.propertyNames = this.process(def.keyType, {
|
|
416
426
|
...params,
|
|
417
427
|
path: [...params.path, "propertyNames"],
|
package/src/v4/core/versions.ts
CHANGED
|
@@ -101,7 +101,7 @@ class JSONSchemaGenerator {
|
|
|
101
101
|
else
|
|
102
102
|
json.type = "number";
|
|
103
103
|
if (typeof exclusiveMinimum === "number") {
|
|
104
|
-
if (this.target === "draft-4") {
|
|
104
|
+
if (this.target === "draft-4" || this.target === "openapi-3.0") {
|
|
105
105
|
json.minimum = exclusiveMinimum;
|
|
106
106
|
json.exclusiveMinimum = true;
|
|
107
107
|
}
|
|
@@ -119,7 +119,7 @@ class JSONSchemaGenerator {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
if (typeof exclusiveMaximum === "number") {
|
|
122
|
-
if (this.target === "draft-4") {
|
|
122
|
+
if (this.target === "draft-4" || this.target === "openapi-3.0") {
|
|
123
123
|
json.maximum = exclusiveMaximum;
|
|
124
124
|
json.exclusiveMaximum = true;
|
|
125
125
|
}
|
|
@@ -288,11 +288,16 @@ class JSONSchemaGenerator {
|
|
|
288
288
|
case "tuple": {
|
|
289
289
|
const json = _json;
|
|
290
290
|
json.type = "array";
|
|
291
|
-
const
|
|
291
|
+
const prefixPath = this.target === "draft-2020-12" ? "prefixItems" : "items";
|
|
292
|
+
const restPath = this.target === "draft-2020-12" ? "items" : this.target === "openapi-3.0" ? "items" : "additionalItems";
|
|
293
|
+
const prefixItems = def.items.map((x, i) => this.process(x, {
|
|
294
|
+
...params,
|
|
295
|
+
path: [...params.path, prefixPath, i],
|
|
296
|
+
}));
|
|
292
297
|
const rest = def.rest
|
|
293
298
|
? this.process(def.rest, {
|
|
294
299
|
...params,
|
|
295
|
-
path: [...params.path, "items
|
|
300
|
+
path: [...params.path, restPath, ...(this.target === "openapi-3.0" ? [def.items.length] : [])],
|
|
296
301
|
})
|
|
297
302
|
: null;
|
|
298
303
|
if (this.target === "draft-2020-12") {
|
|
@@ -302,9 +307,11 @@ class JSONSchemaGenerator {
|
|
|
302
307
|
}
|
|
303
308
|
}
|
|
304
309
|
else if (this.target === "openapi-3.0") {
|
|
305
|
-
json.items =
|
|
310
|
+
json.items = {
|
|
311
|
+
anyOf: [...prefixItems],
|
|
312
|
+
};
|
|
306
313
|
if (rest) {
|
|
307
|
-
json.items.push(rest);
|
|
314
|
+
json.items.anyOf.push(rest);
|
|
308
315
|
}
|
|
309
316
|
json.minItems = prefixItems.length;
|
|
310
317
|
if (!rest) {
|
|
@@ -328,7 +335,7 @@ class JSONSchemaGenerator {
|
|
|
328
335
|
case "record": {
|
|
329
336
|
const json = _json;
|
|
330
337
|
json.type = "object";
|
|
331
|
-
if (this.target
|
|
338
|
+
if (this.target === "draft-7" || this.target === "draft-2020-12") {
|
|
332
339
|
json.propertyNames = this.process(def.keyType, {
|
|
333
340
|
...params,
|
|
334
341
|
path: [...params.path, "propertyNames"],
|
|
@@ -97,7 +97,7 @@ export class JSONSchemaGenerator {
|
|
|
97
97
|
else
|
|
98
98
|
json.type = "number";
|
|
99
99
|
if (typeof exclusiveMinimum === "number") {
|
|
100
|
-
if (this.target === "draft-4") {
|
|
100
|
+
if (this.target === "draft-4" || this.target === "openapi-3.0") {
|
|
101
101
|
json.minimum = exclusiveMinimum;
|
|
102
102
|
json.exclusiveMinimum = true;
|
|
103
103
|
}
|
|
@@ -115,7 +115,7 @@ export class JSONSchemaGenerator {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
if (typeof exclusiveMaximum === "number") {
|
|
118
|
-
if (this.target === "draft-4") {
|
|
118
|
+
if (this.target === "draft-4" || this.target === "openapi-3.0") {
|
|
119
119
|
json.maximum = exclusiveMaximum;
|
|
120
120
|
json.exclusiveMaximum = true;
|
|
121
121
|
}
|
|
@@ -284,11 +284,16 @@ export class JSONSchemaGenerator {
|
|
|
284
284
|
case "tuple": {
|
|
285
285
|
const json = _json;
|
|
286
286
|
json.type = "array";
|
|
287
|
-
const
|
|
287
|
+
const prefixPath = this.target === "draft-2020-12" ? "prefixItems" : "items";
|
|
288
|
+
const restPath = this.target === "draft-2020-12" ? "items" : this.target === "openapi-3.0" ? "items" : "additionalItems";
|
|
289
|
+
const prefixItems = def.items.map((x, i) => this.process(x, {
|
|
290
|
+
...params,
|
|
291
|
+
path: [...params.path, prefixPath, i],
|
|
292
|
+
}));
|
|
288
293
|
const rest = def.rest
|
|
289
294
|
? this.process(def.rest, {
|
|
290
295
|
...params,
|
|
291
|
-
path: [...params.path, "items
|
|
296
|
+
path: [...params.path, restPath, ...(this.target === "openapi-3.0" ? [def.items.length] : [])],
|
|
292
297
|
})
|
|
293
298
|
: null;
|
|
294
299
|
if (this.target === "draft-2020-12") {
|
|
@@ -298,9 +303,11 @@ export class JSONSchemaGenerator {
|
|
|
298
303
|
}
|
|
299
304
|
}
|
|
300
305
|
else if (this.target === "openapi-3.0") {
|
|
301
|
-
json.items =
|
|
306
|
+
json.items = {
|
|
307
|
+
anyOf: [...prefixItems],
|
|
308
|
+
};
|
|
302
309
|
if (rest) {
|
|
303
|
-
json.items.push(rest);
|
|
310
|
+
json.items.anyOf.push(rest);
|
|
304
311
|
}
|
|
305
312
|
json.minItems = prefixItems.length;
|
|
306
313
|
if (!rest) {
|
|
@@ -324,7 +331,7 @@ export class JSONSchemaGenerator {
|
|
|
324
331
|
case "record": {
|
|
325
332
|
const json = _json;
|
|
326
333
|
json.type = "object";
|
|
327
|
-
if (this.target
|
|
334
|
+
if (this.target === "draft-7" || this.target === "draft-2020-12") {
|
|
328
335
|
json.propertyNames = this.process(def.keyType, {
|
|
329
336
|
...params,
|
|
330
337
|
path: [...params.path, "propertyNames"],
|
package/v4/core/versions.cjs
CHANGED
package/v4/core/versions.js
CHANGED