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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zod",
3
- "version": "4.1.2",
3
+ "version": "4.1.3",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "Colin McDonnell <zod@colinhacks.com>",
@@ -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 openapi", () => {
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: "openapi-3.0" })).toMatchInlineSnapshot(`
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
- "type": "boolean",
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)]), { unrepresentable: "any" });
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]), { unrepresentable: "any" });
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, { uri: (id) => `https://example.com/${id}.json` });
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, { ...params, path: [...params.path, "prefixItems", i] })
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 = [...prefixItems];
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 !== "draft-4") {
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"],
@@ -1,5 +1,5 @@
1
1
  export const version = {
2
2
  major: 4,
3
3
  minor: 1,
4
- patch: 2 as number,
4
+ patch: 3 as number,
5
5
  } as const;
@@ -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 prefixItems = def.items.map((x, i) => this.process(x, { ...params, path: [...params.path, "prefixItems", i] }));
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 = [...prefixItems];
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 !== "draft-4") {
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 prefixItems = def.items.map((x, i) => this.process(x, { ...params, path: [...params.path, "prefixItems", i] }));
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 = [...prefixItems];
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 !== "draft-4") {
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"],
@@ -4,5 +4,5 @@ exports.version = void 0;
4
4
  exports.version = {
5
5
  major: 4,
6
6
  minor: 1,
7
- patch: 2,
7
+ patch: 3,
8
8
  };
@@ -1,5 +1,5 @@
1
1
  export const version = {
2
2
  major: 4,
3
3
  minor: 1,
4
- patch: 2,
4
+ patch: 3,
5
5
  };