zod 4.2.0-canary.20250825T235836 → 4.2.0-canary.20250826T001136

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.20250825T235836",
3
+ "version": "4.2.0-canary.20250826T001136",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "Colin McDonnell <zod@colinhacks.com>",
@@ -652,6 +652,18 @@ describe("toJSONSchema", () => {
652
652
  `);
653
653
  });
654
654
 
655
+ test("record openapi", () => {
656
+ const schema = z.record(z.string(), z.boolean());
657
+ expect(z.toJSONSchema(schema, { target: "openapi-3.0" })).toMatchInlineSnapshot(`
658
+ {
659
+ "additionalProperties": {
660
+ "type": "boolean",
661
+ },
662
+ "type": "object",
663
+ }
664
+ `);
665
+ });
666
+
655
667
  test("tuple", () => {
656
668
  const schema = z.tuple([z.string(), z.number()]);
657
669
  expect(z.toJSONSchema(schema)).toMatchInlineSnapshot(`
@@ -735,6 +747,95 @@ describe("toJSONSchema", () => {
735
747
  `);
736
748
  });
737
749
 
750
+ test("tuple draft-7", () => {
751
+ const schema = z.tuple([z.string(), z.number()]);
752
+ expect(z.toJSONSchema(schema, { target: "draft-7", io: "input" })).toMatchInlineSnapshot(`
753
+ {
754
+ "$schema": "http://json-schema.org/draft-07/schema#",
755
+ "items": [
756
+ {
757
+ "type": "string",
758
+ },
759
+ {
760
+ "type": "number",
761
+ },
762
+ ],
763
+ "type": "array",
764
+ }
765
+ `);
766
+ });
767
+
768
+ test("tuple with rest draft-7", () => {
769
+ const schema = z.tuple([z.string(), z.number()]).rest(z.boolean());
770
+ expect(z.toJSONSchema(schema, { target: "draft-7", io: "input" })).toMatchInlineSnapshot(`
771
+ {
772
+ "$schema": "http://json-schema.org/draft-07/schema#",
773
+ "additionalItems": {
774
+ "type": "boolean",
775
+ },
776
+ "items": [
777
+ {
778
+ "type": "string",
779
+ },
780
+ {
781
+ "type": "number",
782
+ },
783
+ ],
784
+ "type": "array",
785
+ }
786
+ `);
787
+ });
788
+
789
+ test("tuple with rest draft-7 - issue #5151 regression test", () => {
790
+ // This test addresses issue #5151: tuple with rest elements and ids
791
+ // in draft-7 had incorrect internal path handling affecting complex scenarios
792
+ const primarySchema = z.string().meta({ id: "primary" });
793
+ const restSchema = z.number().meta({ id: "rest" });
794
+ const testSchema = z.tuple([primarySchema], restSchema);
795
+
796
+ // Test both final output structure AND internal path handling
797
+ const capturedPaths: string[] = [];
798
+ const result = z.toJSONSchema(testSchema, {
799
+ target: "draft-7",
800
+ override: (ctx) => capturedPaths.push(ctx.path.join("/")),
801
+ });
802
+
803
+ // Verify correct draft-7 structure with metadata extraction
804
+ expect(result).toMatchInlineSnapshot(`
805
+ {
806
+ "$schema": "http://json-schema.org/draft-07/schema#",
807
+ "additionalItems": {
808
+ "$ref": "#/definitions/rest",
809
+ },
810
+ "definitions": {
811
+ "primary": {
812
+ "id": "primary",
813
+ "type": "string",
814
+ },
815
+ "rest": {
816
+ "id": "rest",
817
+ "type": "number",
818
+ },
819
+ },
820
+ "items": [
821
+ {
822
+ "$ref": "#/definitions/primary",
823
+ },
824
+ ],
825
+ "type": "array",
826
+ }
827
+ `);
828
+
829
+ // Verify internal paths are correct (this was the actual bug)
830
+ expect(capturedPaths).toContain("items/0"); // prefix items should use "items" path
831
+ expect(capturedPaths).toContain("additionalItems"); // rest should use "additionalItems" path
832
+ expect(capturedPaths).not.toContain("prefixItems/0"); // should not use draft-2020-12 paths
833
+
834
+ // Structural validations
835
+ expect(Array.isArray(result.items)).toBe(true);
836
+ expect(result.additionalItems).toBeDefined();
837
+ });
838
+
738
839
  test("promise", () => {
739
840
  const schema = z.promise(z.string());
740
841
  expect(z.toJSONSchema(schema)).toMatchInlineSnapshot(`
@@ -1558,7 +1659,9 @@ test("unrepresentable literal values are ignored", () => {
1558
1659
  }
1559
1660
  `);
1560
1661
 
1561
- const b = z.z.toJSONSchema(z.literal([undefined, null, 5, BigInt(1324)]), { unrepresentable: "any" });
1662
+ const b = z.z.toJSONSchema(z.literal([undefined, null, 5, BigInt(1324)]), {
1663
+ unrepresentable: "any",
1664
+ });
1562
1665
  expect(b).toMatchInlineSnapshot(`
1563
1666
  {
1564
1667
  "$schema": "https://json-schema.org/draft/2020-12/schema",
@@ -1570,7 +1673,9 @@ test("unrepresentable literal values are ignored", () => {
1570
1673
  }
1571
1674
  `);
1572
1675
 
1573
- const c = z.z.toJSONSchema(z.literal([undefined]), { unrepresentable: "any" });
1676
+ const c = z.z.toJSONSchema(z.literal([undefined]), {
1677
+ unrepresentable: "any",
1678
+ });
1574
1679
  expect(c).toMatchInlineSnapshot(`
1575
1680
  {
1576
1681
  "$schema": "https://json-schema.org/draft/2020-12/schema",
@@ -1853,7 +1958,9 @@ test("basic registry", () => {
1853
1958
  myRegistry.add(User, { id: "User" });
1854
1959
  myRegistry.add(Post, { id: "Post" });
1855
1960
 
1856
- const result = z.z.toJSONSchema(myRegistry, { uri: (id) => `https://example.com/${id}.json` });
1961
+ const result = z.z.toJSONSchema(myRegistry, {
1962
+ uri: (id) => `https://example.com/${id}.json`,
1963
+ });
1857
1964
  expect(result).toMatchInlineSnapshot(`
1858
1965
  {
1859
1966
  "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
 
@@ -413,7 +421,7 @@ export class JSONSchemaGenerator {
413
421
  case "record": {
414
422
  const json: JSONSchema.ObjectSchema = _json as any;
415
423
  json.type = "object";
416
- if (this.target !== "draft-4") {
424
+ if (this.target === "draft-7" || this.target === "draft-2020-12") {
417
425
  json.propertyNames = this.process(def.keyType, {
418
426
  ...params,
419
427
  path: [...params.path, "propertyNames"],
@@ -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") {
@@ -330,7 +335,7 @@ class JSONSchemaGenerator {
330
335
  case "record": {
331
336
  const json = _json;
332
337
  json.type = "object";
333
- if (this.target !== "draft-4") {
338
+ if (this.target === "draft-7" || this.target === "draft-2020-12") {
334
339
  json.propertyNames = this.process(def.keyType, {
335
340
  ...params,
336
341
  path: [...params.path, "propertyNames"],
@@ -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") {
@@ -326,7 +331,7 @@ export class JSONSchemaGenerator {
326
331
  case "record": {
327
332
  const json = _json;
328
333
  json.type = "object";
329
- if (this.target !== "draft-4") {
334
+ if (this.target === "draft-7" || this.target === "draft-2020-12") {
330
335
  json.propertyNames = this.process(def.keyType, {
331
336
  ...params,
332
337
  path: [...params.path, "propertyNames"],