zod 4.2.0-canary.20250826T001136 → 4.2.0-canary.20250827T070334
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 +94 -10
- package/src/v4/core/to-json-schema.ts +11 -18
- package/src/v4/core/versions.ts +1 -1
- package/v4/core/to-json-schema.cjs +12 -21
- package/v4/core/to-json-schema.js +12 -21
- package/v4/core/versions.cjs +1 -1
- package/v4/core/versions.js +1 -1
package/package.json
CHANGED
|
@@ -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,7 +579,7 @@ describe("toJSONSchema", () => {
|
|
|
552
579
|
`);
|
|
553
580
|
});
|
|
554
581
|
|
|
555
|
-
test("nullable openapi", () => {
|
|
582
|
+
test("nullable openapi-3.0", () => {
|
|
556
583
|
expect(z.toJSONSchema(z.string().nullable(), { target: "openapi-3.0" })).toMatchInlineSnapshot(`
|
|
557
584
|
{
|
|
558
585
|
"nullable": true,
|
|
@@ -561,12 +588,35 @@ describe("toJSONSchema", () => {
|
|
|
561
588
|
`);
|
|
562
589
|
});
|
|
563
590
|
|
|
564
|
-
test("union with null openapi", () => {
|
|
591
|
+
test("union with null openapi-3.0", () => {
|
|
565
592
|
const schema = z.union([z.string(), z.null()]);
|
|
566
593
|
expect(z.toJSONSchema(schema, { target: "openapi-3.0" })).toMatchInlineSnapshot(`
|
|
567
594
|
{
|
|
568
|
-
"
|
|
569
|
-
|
|
595
|
+
"anyOf": [
|
|
596
|
+
{
|
|
597
|
+
"type": "string",
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
"enum": [
|
|
601
|
+
null,
|
|
602
|
+
],
|
|
603
|
+
"nullable": true,
|
|
604
|
+
"type": "string",
|
|
605
|
+
},
|
|
606
|
+
],
|
|
607
|
+
}
|
|
608
|
+
`);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
test("number with exclusive min-max openapi-3.0", () => {
|
|
612
|
+
const schema = z.number().lt(100).gt(1);
|
|
613
|
+
expect(z.toJSONSchema(schema, { target: "openapi-3.0" })).toMatchInlineSnapshot(`
|
|
614
|
+
{
|
|
615
|
+
"exclusiveMaximum": true,
|
|
616
|
+
"exclusiveMinimum": true,
|
|
617
|
+
"maximum": 100,
|
|
618
|
+
"minimum": 1,
|
|
619
|
+
"type": "number",
|
|
570
620
|
}
|
|
571
621
|
`);
|
|
572
622
|
});
|
|
@@ -652,7 +702,7 @@ describe("toJSONSchema", () => {
|
|
|
652
702
|
`);
|
|
653
703
|
});
|
|
654
704
|
|
|
655
|
-
test("record openapi", () => {
|
|
705
|
+
test("record openapi-3.0", () => {
|
|
656
706
|
const schema = z.record(z.string(), z.boolean());
|
|
657
707
|
expect(z.toJSONSchema(schema, { target: "openapi-3.0" })).toMatchInlineSnapshot(`
|
|
658
708
|
{
|
|
@@ -703,9 +753,11 @@ describe("toJSONSchema", () => {
|
|
|
703
753
|
`);
|
|
704
754
|
});
|
|
705
755
|
|
|
706
|
-
test("tuple openapi", () => {
|
|
756
|
+
test("tuple openapi-3.0", () => {
|
|
707
757
|
const schema = z.tuple([z.string(), z.number()]);
|
|
708
|
-
|
|
758
|
+
const jsonSchema = z.toJSONSchema(schema, { target: "openapi-3.0" });
|
|
759
|
+
validateOpenAPI30Schema(jsonSchema);
|
|
760
|
+
expect(jsonSchema).toMatchInlineSnapshot(`
|
|
709
761
|
{
|
|
710
762
|
"items": {
|
|
711
763
|
"anyOf": [
|
|
@@ -724,9 +776,11 @@ describe("toJSONSchema", () => {
|
|
|
724
776
|
`);
|
|
725
777
|
});
|
|
726
778
|
|
|
727
|
-
test("tuple with rest openapi", () => {
|
|
779
|
+
test("tuple with rest openapi-3.0", () => {
|
|
728
780
|
const schema = z.tuple([z.string(), z.number()]).rest(z.boolean());
|
|
729
|
-
|
|
781
|
+
const jsonSchema = z.toJSONSchema(schema, { target: "openapi-3.0" });
|
|
782
|
+
validateOpenAPI30Schema(jsonSchema);
|
|
783
|
+
expect(jsonSchema).toMatchInlineSnapshot(`
|
|
730
784
|
{
|
|
731
785
|
"items": {
|
|
732
786
|
"anyOf": [
|
|
@@ -741,7 +795,37 @@ describe("toJSONSchema", () => {
|
|
|
741
795
|
},
|
|
742
796
|
],
|
|
743
797
|
},
|
|
744
|
-
"minItems":
|
|
798
|
+
"minItems": 3,
|
|
799
|
+
"type": "array",
|
|
800
|
+
}
|
|
801
|
+
`);
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
test("tuple with null openapi-3.0", () => {
|
|
805
|
+
const schema = z.tuple([z.string(), z.number(), z.null()]);
|
|
806
|
+
const jsonSchema = z.toJSONSchema(schema, { target: "openapi-3.0" });
|
|
807
|
+
validateOpenAPI30Schema(jsonSchema);
|
|
808
|
+
expect(jsonSchema).toMatchInlineSnapshot(`
|
|
809
|
+
{
|
|
810
|
+
"items": {
|
|
811
|
+
"anyOf": [
|
|
812
|
+
{
|
|
813
|
+
"type": "string",
|
|
814
|
+
},
|
|
815
|
+
{
|
|
816
|
+
"type": "number",
|
|
817
|
+
},
|
|
818
|
+
{
|
|
819
|
+
"enum": [
|
|
820
|
+
null,
|
|
821
|
+
],
|
|
822
|
+
"nullable": true,
|
|
823
|
+
"type": "string",
|
|
824
|
+
},
|
|
825
|
+
],
|
|
826
|
+
},
|
|
827
|
+
"maxItems": 3,
|
|
828
|
+
"minItems": 3,
|
|
745
829
|
"type": "array",
|
|
746
830
|
}
|
|
747
831
|
`);
|
|
@@ -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 {
|
|
@@ -236,7 +236,11 @@ export class JSONSchemaGenerator {
|
|
|
236
236
|
break;
|
|
237
237
|
}
|
|
238
238
|
case "null": {
|
|
239
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
}
|
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
|
}
|
|
@@ -158,7 +158,13 @@ class JSONSchemaGenerator {
|
|
|
158
158
|
break;
|
|
159
159
|
}
|
|
160
160
|
case "null": {
|
|
161
|
-
|
|
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
|
-
|
|
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:
|
|
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" }];
|
|
@@ -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
|
}
|
|
@@ -154,7 +154,13 @@ export class JSONSchemaGenerator {
|
|
|
154
154
|
break;
|
|
155
155
|
}
|
|
156
156
|
case "null": {
|
|
157
|
-
|
|
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
|
-
|
|
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:
|
|
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" }];
|
package/v4/core/versions.cjs
CHANGED
package/v4/core/versions.js
CHANGED