zod 4.1.3 → 4.1.5

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.3",
3
+ "version": "4.1.5",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "Colin McDonnell <zod@colinhacks.com>",
@@ -2059,11 +2059,11 @@ export const ZodFunction: core.$constructor<ZodFunction> = /*@__PURE__*/ core.$c
2059
2059
  );
2060
2060
 
2061
2061
  export function _function(): ZodFunction;
2062
- export function _function<const In extends Array<core.$ZodType> = Array<core.$ZodType>>(params: {
2062
+ export function _function<const In extends ReadonlyArray<core.$ZodType>>(params: {
2063
2063
  input: In;
2064
2064
  }): ZodFunction<ZodTuple<In, null>, core.$ZodFunctionOut>;
2065
2065
  export function _function<
2066
- const In extends Array<core.$ZodType> = Array<core.$ZodType>,
2066
+ const In extends ReadonlyArray<core.$ZodType>,
2067
2067
  const Out extends core.$ZodFunctionOut = core.$ZodFunctionOut,
2068
2068
  >(params: {
2069
2069
  input: In;
@@ -130,6 +130,46 @@ test("valid function run", () => {
130
130
  });
131
131
  });
132
132
 
133
+ const args3 = [
134
+ z.object({
135
+ f1: z.number(),
136
+ f2: z.string().nullable(),
137
+ f3: z.array(z.boolean().optional()).optional(),
138
+ }),
139
+ ] as const;
140
+ const returns3 = z.union([z.string(), z.number()]);
141
+
142
+ const func3 = z.function({
143
+ input: args3,
144
+ output: returns3,
145
+ });
146
+
147
+ test("function inference 3", () => {
148
+ type func3 = (typeof func3)["_input"];
149
+
150
+ expectTypeOf<func3>().toEqualTypeOf<
151
+ (arg: {
152
+ f3?: (boolean | undefined)[] | undefined;
153
+ f1: number;
154
+ f2: string | null;
155
+ }) => string | number
156
+ >();
157
+ });
158
+
159
+ test("valid function run", () => {
160
+ const validFunc3Instance = func3.implement((_x) => {
161
+ _x.f2;
162
+ _x.f3![0];
163
+ return "adf" as any;
164
+ });
165
+
166
+ validFunc3Instance({
167
+ f1: 21,
168
+ f2: "asdf",
169
+ f3: [true, false],
170
+ });
171
+ });
172
+
133
173
  test("input validation error", () => {
134
174
  const schema = z.function({
135
175
  input: z.tuple([z.string()]),
@@ -1,7 +1,34 @@
1
+ import { Validator } from "@seriousme/openapi-schema-validator";
1
2
  import { describe, expect, test } from "vitest";
2
3
  import * as z from "zod/v4";
3
4
  // import * as zCore from "zod/v4/core";
4
5
 
6
+ const openAPI30Validator = new Validator();
7
+ /** @see https://github.com/colinhacks/zod/issues/5147 */
8
+ const validateOpenAPI30Schema = async (zodJSONSchema: Record<string, unknown>): Promise<true> => {
9
+ const res = await openAPI30Validator.validate({
10
+ openapi: "3.0.0",
11
+ info: {
12
+ title: "SampleApi",
13
+ description: "Sample backend service",
14
+ version: "1.0.0",
15
+ },
16
+ components: { schemas: { test: zodJSONSchema } },
17
+ paths: {},
18
+ });
19
+
20
+ if (!res.valid) {
21
+ // `console.error` should make `vitest` trow an unhandled error
22
+ // printing the validation messages in consoles
23
+ console.error(
24
+ `OpenAPI schema is not valid against ${openAPI30Validator.version}`,
25
+ JSON.stringify(res.errors, null, 2)
26
+ );
27
+ }
28
+
29
+ return true;
30
+ };
31
+
5
32
  describe("toJSONSchema", () => {
6
33
  test("primitive types", () => {
7
34
  expect(z.toJSONSchema(z.string())).toMatchInlineSnapshot(`
@@ -552,8 +579,11 @@ describe("toJSONSchema", () => {
552
579
  `);
553
580
  });
554
581
 
555
- test("nullable openapi", () => {
556
- expect(z.toJSONSchema(z.string().nullable(), { target: "openapi-3.0" })).toMatchInlineSnapshot(`
582
+ test("nullable openapi-3.0", () => {
583
+ const schema = z.string().nullable();
584
+ const jsonSchema = z.toJSONSchema(schema, { target: "openapi-3.0" });
585
+ validateOpenAPI30Schema(jsonSchema);
586
+ expect(jsonSchema).toMatchInlineSnapshot(`
557
587
  {
558
588
  "nullable": true,
559
589
  "type": "string",
@@ -561,19 +591,33 @@ describe("toJSONSchema", () => {
561
591
  `);
562
592
  });
563
593
 
564
- test("union with null openapi", () => {
594
+ test("union with null openapi-3.0", () => {
565
595
  const schema = z.union([z.string(), z.null()]);
566
- expect(z.toJSONSchema(schema, { target: "openapi-3.0" })).toMatchInlineSnapshot(`
596
+ const jsonSchema = z.toJSONSchema(schema, { target: "openapi-3.0" });
597
+ validateOpenAPI30Schema(jsonSchema);
598
+ expect(jsonSchema).toMatchInlineSnapshot(`
567
599
  {
568
- "nullable": true,
569
- "type": "string",
600
+ "anyOf": [
601
+ {
602
+ "type": "string",
603
+ },
604
+ {
605
+ "enum": [
606
+ null,
607
+ ],
608
+ "nullable": true,
609
+ "type": "string",
610
+ },
611
+ ],
570
612
  }
571
613
  `);
572
614
  });
573
615
 
574
- test("number with exclusive min-max openapi", () => {
616
+ test("number with exclusive min-max openapi-3.0", () => {
575
617
  const schema = z.number().lt(100).gt(1);
576
- expect(z.toJSONSchema(schema, { target: "openapi-3.0" })).toMatchInlineSnapshot(`
618
+ const jsonSchema = z.toJSONSchema(schema, { target: "openapi-3.0" });
619
+ validateOpenAPI30Schema(jsonSchema);
620
+ expect(jsonSchema).toMatchInlineSnapshot(`
577
621
  {
578
622
  "exclusiveMaximum": true,
579
623
  "exclusiveMinimum": true,
@@ -665,9 +709,11 @@ describe("toJSONSchema", () => {
665
709
  `);
666
710
  });
667
711
 
668
- test("record openapi", () => {
712
+ test("record openapi-3.0", () => {
669
713
  const schema = z.record(z.string(), z.boolean());
670
- expect(z.toJSONSchema(schema, { target: "openapi-3.0" })).toMatchInlineSnapshot(`
714
+ const jsonSchema = z.toJSONSchema(schema, { target: "openapi-3.0" });
715
+ validateOpenAPI30Schema(jsonSchema);
716
+ expect(jsonSchema).toMatchInlineSnapshot(`
671
717
  {
672
718
  "additionalProperties": {
673
719
  "type": "boolean",
@@ -716,9 +762,11 @@ describe("toJSONSchema", () => {
716
762
  `);
717
763
  });
718
764
 
719
- test("tuple openapi", () => {
765
+ test("tuple openapi-3.0", () => {
720
766
  const schema = z.tuple([z.string(), z.number()]);
721
- expect(z.toJSONSchema(schema, { target: "openapi-3.0" })).toMatchInlineSnapshot(`
767
+ const jsonSchema = z.toJSONSchema(schema, { target: "openapi-3.0" });
768
+ validateOpenAPI30Schema(jsonSchema);
769
+ expect(jsonSchema).toMatchInlineSnapshot(`
722
770
  {
723
771
  "items": {
724
772
  "anyOf": [
@@ -737,9 +785,11 @@ describe("toJSONSchema", () => {
737
785
  `);
738
786
  });
739
787
 
740
- test("tuple with rest openapi", () => {
788
+ test("tuple with rest openapi-3.0", () => {
741
789
  const schema = z.tuple([z.string(), z.number()]).rest(z.boolean());
742
- expect(z.toJSONSchema(schema, { target: "openapi-3.0" })).toMatchInlineSnapshot(`
790
+ const jsonSchema = z.toJSONSchema(schema, { target: "openapi-3.0" });
791
+ validateOpenAPI30Schema(jsonSchema);
792
+ expect(jsonSchema).toMatchInlineSnapshot(`
743
793
  {
744
794
  "items": {
745
795
  "anyOf": [
@@ -754,7 +804,37 @@ describe("toJSONSchema", () => {
754
804
  },
755
805
  ],
756
806
  },
757
- "minItems": 2,
807
+ "minItems": 3,
808
+ "type": "array",
809
+ }
810
+ `);
811
+ });
812
+
813
+ test("tuple with null openapi-3.0", () => {
814
+ const schema = z.tuple([z.string(), z.number(), z.null()]);
815
+ const jsonSchema = z.toJSONSchema(schema, { target: "openapi-3.0" });
816
+ validateOpenAPI30Schema(jsonSchema);
817
+ expect(jsonSchema).toMatchInlineSnapshot(`
818
+ {
819
+ "items": {
820
+ "anyOf": [
821
+ {
822
+ "type": "string",
823
+ },
824
+ {
825
+ "type": "number",
826
+ },
827
+ {
828
+ "enum": [
829
+ null,
830
+ ],
831
+ "nullable": true,
832
+ "type": "string",
833
+ },
834
+ ],
835
+ },
836
+ "maxItems": 3,
837
+ "minItems": 3,
758
838
  "type": "array",
759
839
  }
760
840
  `);
@@ -236,7 +236,11 @@ export class JSONSchemaGenerator {
236
236
  break;
237
237
  }
238
238
  case "null": {
239
- _json.type = "null";
239
+ if (this.target === "openapi-3.0") {
240
+ _json.type = "string";
241
+ _json.nullable = true;
242
+ _json.enum = [null];
243
+ } else _json.type = "null";
240
244
  break;
241
245
  }
242
246
  case "any": {
@@ -332,18 +336,7 @@ export class JSONSchemaGenerator {
332
336
  path: [...params.path, "anyOf", i],
333
337
  })
334
338
  );
335
- if (this.target === "openapi-3.0") {
336
- const nonNull = options.filter((x) => (x as any).type !== "null");
337
- const hasNull = nonNull.length !== options.length;
338
- if (nonNull.length === 1) {
339
- Object.assign(json, nonNull[0]!);
340
- } else {
341
- json.anyOf = nonNull;
342
- }
343
- if (hasNull) (json as any).nullable = true;
344
- } else {
345
- json.anyOf = options;
346
- }
339
+ json.anyOf = options;
347
340
  break;
348
341
  }
349
342
  case "intersection": {
@@ -393,8 +386,9 @@ export class JSONSchemaGenerator {
393
386
  }
394
387
  } else if (this.target === "openapi-3.0") {
395
388
  json.items = {
396
- anyOf: [...prefixItems],
389
+ anyOf: prefixItems,
397
390
  };
391
+
398
392
  if (rest) {
399
393
  json.items.anyOf!.push(rest);
400
394
  }
@@ -534,9 +528,8 @@ export class JSONSchemaGenerator {
534
528
  case "nullable": {
535
529
  const inner = this.process(def.innerType, params);
536
530
  if (this.target === "openapi-3.0") {
537
- Object.assign(_json, inner);
538
- (_json as any).nullable = true;
539
531
  result.ref = def.innerType;
532
+ _json.nullable = true;
540
533
  } else {
541
534
  _json.anyOf = [inner, { type: "null" }];
542
535
  }
@@ -1,5 +1,5 @@
1
1
  export const version = {
2
2
  major: 4,
3
3
  minor: 1,
4
- patch: 3 as number,
4
+ patch: 5 as number,
5
5
  } as const;
@@ -624,10 +624,10 @@ export interface ZodFunction<Args extends core.$ZodFunctionIn = core.$ZodFunctio
624
624
  }
625
625
  export declare const ZodFunction: core.$constructor<ZodFunction>;
626
626
  export declare function _function(): ZodFunction;
627
- export declare function _function<const In extends Array<core.$ZodType> = Array<core.$ZodType>>(params: {
627
+ export declare function _function<const In extends ReadonlyArray<core.$ZodType>>(params: {
628
628
  input: In;
629
629
  }): ZodFunction<ZodTuple<In, null>, core.$ZodFunctionOut>;
630
- export declare function _function<const In extends Array<core.$ZodType> = Array<core.$ZodType>, const Out extends core.$ZodFunctionOut = core.$ZodFunctionOut>(params: {
630
+ export declare function _function<const In extends ReadonlyArray<core.$ZodType>, const Out extends core.$ZodFunctionOut = core.$ZodFunctionOut>(params: {
631
631
  input: In;
632
632
  output: Out;
633
633
  }): ZodFunction<ZodTuple<In, null>, Out>;
@@ -624,10 +624,10 @@ export interface ZodFunction<Args extends core.$ZodFunctionIn = core.$ZodFunctio
624
624
  }
625
625
  export declare const ZodFunction: core.$constructor<ZodFunction>;
626
626
  export declare function _function(): ZodFunction;
627
- export declare function _function<const In extends Array<core.$ZodType> = Array<core.$ZodType>>(params: {
627
+ export declare function _function<const In extends ReadonlyArray<core.$ZodType>>(params: {
628
628
  input: In;
629
629
  }): ZodFunction<ZodTuple<In, null>, core.$ZodFunctionOut>;
630
- export declare function _function<const In extends Array<core.$ZodType> = Array<core.$ZodType>, const Out extends core.$ZodFunctionOut = core.$ZodFunctionOut>(params: {
630
+ export declare function _function<const In extends ReadonlyArray<core.$ZodType>, const Out extends core.$ZodFunctionOut = core.$ZodFunctionOut>(params: {
631
631
  input: In;
632
632
  output: Out;
633
633
  }): ZodFunction<ZodTuple<In, null>, Out>;
@@ -158,7 +158,13 @@ class JSONSchemaGenerator {
158
158
  break;
159
159
  }
160
160
  case "null": {
161
- _json.type = "null";
161
+ if (this.target === "openapi-3.0") {
162
+ _json.type = "string";
163
+ _json.nullable = true;
164
+ _json.enum = [null];
165
+ }
166
+ else
167
+ _json.type = "null";
162
168
  break;
163
169
  }
164
170
  case "any": {
@@ -250,21 +256,7 @@ class JSONSchemaGenerator {
250
256
  ...params,
251
257
  path: [...params.path, "anyOf", i],
252
258
  }));
253
- if (this.target === "openapi-3.0") {
254
- const nonNull = options.filter((x) => x.type !== "null");
255
- const hasNull = nonNull.length !== options.length;
256
- if (nonNull.length === 1) {
257
- Object.assign(json, nonNull[0]);
258
- }
259
- else {
260
- json.anyOf = nonNull;
261
- }
262
- if (hasNull)
263
- json.nullable = true;
264
- }
265
- else {
266
- json.anyOf = options;
267
- }
259
+ json.anyOf = options;
268
260
  break;
269
261
  }
270
262
  case "intersection": {
@@ -308,7 +300,7 @@ class JSONSchemaGenerator {
308
300
  }
309
301
  else if (this.target === "openapi-3.0") {
310
302
  json.items = {
311
- anyOf: [...prefixItems],
303
+ anyOf: prefixItems,
312
304
  };
313
305
  if (rest) {
314
306
  json.items.anyOf.push(rest);
@@ -461,9 +453,8 @@ class JSONSchemaGenerator {
461
453
  case "nullable": {
462
454
  const inner = this.process(def.innerType, params);
463
455
  if (this.target === "openapi-3.0") {
464
- Object.assign(_json, inner);
465
- _json.nullable = true;
466
456
  result.ref = def.innerType;
457
+ _json.nullable = true;
467
458
  }
468
459
  else {
469
460
  _json.anyOf = [inner, { type: "null" }];
@@ -154,7 +154,13 @@ export class JSONSchemaGenerator {
154
154
  break;
155
155
  }
156
156
  case "null": {
157
- _json.type = "null";
157
+ if (this.target === "openapi-3.0") {
158
+ _json.type = "string";
159
+ _json.nullable = true;
160
+ _json.enum = [null];
161
+ }
162
+ else
163
+ _json.type = "null";
158
164
  break;
159
165
  }
160
166
  case "any": {
@@ -246,21 +252,7 @@ export class JSONSchemaGenerator {
246
252
  ...params,
247
253
  path: [...params.path, "anyOf", i],
248
254
  }));
249
- if (this.target === "openapi-3.0") {
250
- const nonNull = options.filter((x) => x.type !== "null");
251
- const hasNull = nonNull.length !== options.length;
252
- if (nonNull.length === 1) {
253
- Object.assign(json, nonNull[0]);
254
- }
255
- else {
256
- json.anyOf = nonNull;
257
- }
258
- if (hasNull)
259
- json.nullable = true;
260
- }
261
- else {
262
- json.anyOf = options;
263
- }
255
+ json.anyOf = options;
264
256
  break;
265
257
  }
266
258
  case "intersection": {
@@ -304,7 +296,7 @@ export class JSONSchemaGenerator {
304
296
  }
305
297
  else if (this.target === "openapi-3.0") {
306
298
  json.items = {
307
- anyOf: [...prefixItems],
299
+ anyOf: prefixItems,
308
300
  };
309
301
  if (rest) {
310
302
  json.items.anyOf.push(rest);
@@ -457,9 +449,8 @@ export class JSONSchemaGenerator {
457
449
  case "nullable": {
458
450
  const inner = this.process(def.innerType, params);
459
451
  if (this.target === "openapi-3.0") {
460
- Object.assign(_json, inner);
461
- _json.nullable = true;
462
452
  result.ref = def.innerType;
453
+ _json.nullable = true;
463
454
  }
464
455
  else {
465
456
  _json.anyOf = [inner, { type: "null" }];
@@ -4,5 +4,5 @@ exports.version = void 0;
4
4
  exports.version = {
5
5
  major: 4,
6
6
  minor: 1,
7
- patch: 3,
7
+ patch: 5,
8
8
  };
@@ -1,5 +1,5 @@
1
1
  export const version = {
2
2
  major: 4,
3
3
  minor: 1,
4
- patch: 3,
4
+ patch: 5,
5
5
  };