zod 4.2.0-canary.20250825T230433 → 4.2.0-canary.20250826T000512

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.2.0-canary.20250825T230433",
3
+ "version": "4.2.0-canary.20250826T000512",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "Colin McDonnell <zod@colinhacks.com>",
@@ -695,6 +695,51 @@ describe("toJSONSchema", () => {
695
695
  const schema = z.tuple([z.string(), z.number()]);
696
696
  expect(z.toJSONSchema(schema, { target: "openapi-3.0" })).toMatchInlineSnapshot(`
697
697
  {
698
+ "items": {
699
+ "anyOf": [
700
+ {
701
+ "type": "string",
702
+ },
703
+ {
704
+ "type": "number",
705
+ },
706
+ ],
707
+ },
708
+ "maxItems": 2,
709
+ "minItems": 2,
710
+ "type": "array",
711
+ }
712
+ `);
713
+ });
714
+
715
+ test("tuple with rest openapi", () => {
716
+ const schema = z.tuple([z.string(), z.number()]).rest(z.boolean());
717
+ expect(z.toJSONSchema(schema, { target: "openapi-3.0" })).toMatchInlineSnapshot(`
718
+ {
719
+ "items": {
720
+ "anyOf": [
721
+ {
722
+ "type": "string",
723
+ },
724
+ {
725
+ "type": "number",
726
+ },
727
+ {
728
+ "type": "boolean",
729
+ },
730
+ ],
731
+ },
732
+ "minItems": 2,
733
+ "type": "array",
734
+ }
735
+ `);
736
+ });
737
+
738
+ test("tuple draft-7", () => {
739
+ const schema = z.tuple([z.string(), z.number()]);
740
+ expect(z.toJSONSchema(schema, { target: "draft-7", io: "input" })).toMatchInlineSnapshot(`
741
+ {
742
+ "$schema": "http://json-schema.org/draft-07/schema#",
698
743
  "items": [
699
744
  {
700
745
  "type": "string",
@@ -703,17 +748,19 @@ describe("toJSONSchema", () => {
703
748
  "type": "number",
704
749
  },
705
750
  ],
706
- "maxItems": 2,
707
- "minItems": 2,
708
751
  "type": "array",
709
752
  }
710
753
  `);
711
754
  });
712
755
 
713
- test("tuple with rest openapi", () => {
756
+ test("tuple with rest draft-7", () => {
714
757
  const schema = z.tuple([z.string(), z.number()]).rest(z.boolean());
715
- expect(z.toJSONSchema(schema, { target: "openapi-3.0" })).toMatchInlineSnapshot(`
758
+ expect(z.toJSONSchema(schema, { target: "draft-7", io: "input" })).toMatchInlineSnapshot(`
716
759
  {
760
+ "$schema": "http://json-schema.org/draft-07/schema#",
761
+ "additionalItems": {
762
+ "type": "boolean",
763
+ },
717
764
  "items": [
718
765
  {
719
766
  "type": "string",
@@ -721,14 +768,60 @@ describe("toJSONSchema", () => {
721
768
  {
722
769
  "type": "number",
723
770
  },
771
+ ],
772
+ "type": "array",
773
+ }
774
+ `);
775
+ });
776
+
777
+ test("tuple with rest draft-7 - issue #5151 regression test", () => {
778
+ // This test addresses issue #5151: tuple with rest elements and ids
779
+ // in draft-7 had incorrect internal path handling affecting complex scenarios
780
+ const primarySchema = z.string().meta({ id: "primary" });
781
+ const restSchema = z.number().meta({ id: "rest" });
782
+ const testSchema = z.tuple([primarySchema], restSchema);
783
+
784
+ // Test both final output structure AND internal path handling
785
+ const capturedPaths: string[] = [];
786
+ const result = z.toJSONSchema(testSchema, {
787
+ target: "draft-7",
788
+ override: (ctx) => capturedPaths.push(ctx.path.join("/")),
789
+ });
790
+
791
+ // Verify correct draft-7 structure with metadata extraction
792
+ expect(result).toMatchInlineSnapshot(`
793
+ {
794
+ "$schema": "http://json-schema.org/draft-07/schema#",
795
+ "additionalItems": {
796
+ "$ref": "#/definitions/rest",
797
+ },
798
+ "definitions": {
799
+ "primary": {
800
+ "id": "primary",
801
+ "type": "string",
802
+ },
803
+ "rest": {
804
+ "id": "rest",
805
+ "type": "number",
806
+ },
807
+ },
808
+ "items": [
724
809
  {
725
- "type": "boolean",
810
+ "$ref": "#/definitions/primary",
726
811
  },
727
812
  ],
728
- "minItems": 2,
729
813
  "type": "array",
730
814
  }
731
815
  `);
816
+
817
+ // Verify internal paths are correct (this was the actual bug)
818
+ expect(capturedPaths).toContain("items/0"); // prefix items should use "items" path
819
+ expect(capturedPaths).toContain("additionalItems"); // rest should use "additionalItems" path
820
+ expect(capturedPaths).not.toContain("prefixItems/0"); // should not use draft-2020-12 paths
821
+
822
+ // Structural validations
823
+ expect(Array.isArray(result.items)).toBe(true);
824
+ expect(result.additionalItems).toBeDefined();
732
825
  });
733
826
 
734
827
  test("promise", () => {
@@ -1554,7 +1647,9 @@ test("unrepresentable literal values are ignored", () => {
1554
1647
  }
1555
1648
  `);
1556
1649
 
1557
- const b = z.z.toJSONSchema(z.literal([undefined, null, 5, BigInt(1324)]), { unrepresentable: "any" });
1650
+ const b = z.z.toJSONSchema(z.literal([undefined, null, 5, BigInt(1324)]), {
1651
+ unrepresentable: "any",
1652
+ });
1558
1653
  expect(b).toMatchInlineSnapshot(`
1559
1654
  {
1560
1655
  "$schema": "https://json-schema.org/draft/2020-12/schema",
@@ -1566,7 +1661,9 @@ test("unrepresentable literal values are ignored", () => {
1566
1661
  }
1567
1662
  `);
1568
1663
 
1569
- const c = z.z.toJSONSchema(z.literal([undefined]), { unrepresentable: "any" });
1664
+ const c = z.z.toJSONSchema(z.literal([undefined]), {
1665
+ unrepresentable: "any",
1666
+ });
1570
1667
  expect(c).toMatchInlineSnapshot(`
1571
1668
  {
1572
1669
  "$schema": "https://json-schema.org/draft/2020-12/schema",
@@ -1849,7 +1946,9 @@ test("basic registry", () => {
1849
1946
  myRegistry.add(User, { id: "User" });
1850
1947
  myRegistry.add(Post, { id: "Post" });
1851
1948
 
1852
- const result = z.z.toJSONSchema(myRegistry, { uri: (id) => `https://example.com/${id}.json` });
1949
+ const result = z.z.toJSONSchema(myRegistry, {
1950
+ uri: (id) => `https://example.com/${id}.json`,
1951
+ });
1853
1952
  expect(result).toMatchInlineSnapshot(`
1854
1953
  {
1855
1954
  "schemas": {
@@ -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) {
@@ -1,5 +1,5 @@
1
1
  export const version = {
2
2
  major: 4,
3
3
  minor: 1,
4
- patch: 1 as number,
4
+ patch: 2 as number,
5
5
  } as const;
@@ -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) {
@@ -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) {
@@ -4,5 +4,5 @@ exports.version = void 0;
4
4
  exports.version = {
5
5
  major: 4,
6
6
  minor: 1,
7
- patch: 1,
7
+ patch: 2,
8
8
  };
@@ -1,5 +1,5 @@
1
1
  export const version = {
2
2
  major: 4,
3
3
  minor: 1,
4
- patch: 1,
4
+ patch: 2,
5
5
  };