@alt-stack/zod-openapi 1.0.1

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.
@@ -0,0 +1,480 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { convertSchemaToZodString } from "./to-zod";
3
+ import { openApiToZodTsCode } from "./to-typescript";
4
+ import { clearZodSchemaToOpenApiSchemaRegistry } from "./registry";
5
+
6
+ describe("convertSchemaToZodString", () => {
7
+ beforeEach(() => {
8
+ clearZodSchemaToOpenApiSchemaRegistry();
9
+ });
10
+
11
+ afterEach(() => {
12
+ clearZodSchemaToOpenApiSchemaRegistry();
13
+ });
14
+
15
+ describe("basic types", () => {
16
+ it("should convert string schema", () => {
17
+ const result = convertSchemaToZodString({ type: "string" });
18
+ expect(result).toBe("z.string()");
19
+ });
20
+
21
+ it("should convert number schema", () => {
22
+ const result = convertSchemaToZodString({ type: "number" });
23
+ expect(result).toBe("z.number()");
24
+ });
25
+
26
+ it("should convert integer schema", () => {
27
+ const result = convertSchemaToZodString({ type: "integer" });
28
+ expect(result).toBe("z.number().int()");
29
+ });
30
+
31
+ it("should convert boolean schema", () => {
32
+ const result = convertSchemaToZodString({ type: "boolean" });
33
+ expect(result).toBe("z.boolean()");
34
+ });
35
+ });
36
+
37
+ describe("complex types", () => {
38
+ it("should convert array schema", () => {
39
+ const result = convertSchemaToZodString({
40
+ type: "array",
41
+ items: { type: "string" },
42
+ });
43
+ expect(result).toBe("z.array(z.string())");
44
+ });
45
+
46
+ it("should convert object schema", () => {
47
+ const result = convertSchemaToZodString({
48
+ type: "object",
49
+ properties: {
50
+ name: { type: "string" },
51
+ },
52
+ required: ["name"],
53
+ });
54
+ expect(result).toBe("z.object({ name: z.string() })");
55
+ });
56
+
57
+ it("should convert union schema", () => {
58
+ const result = convertSchemaToZodString({
59
+ oneOf: [{ type: "string" }, { type: "number" }],
60
+ });
61
+ expect(result).toBe("z.union([z.string(), z.number()])");
62
+ });
63
+
64
+ it("should convert intersection schema", () => {
65
+ const result = convertSchemaToZodString({
66
+ allOf: [{ type: "string" }, { type: "number" }],
67
+ });
68
+ expect(result).toBe("z.intersection(z.string(), z.number())");
69
+ });
70
+ });
71
+
72
+ describe("$ref handling", () => {
73
+ it("should convert $ref to schema variable name", () => {
74
+ const result = convertSchemaToZodString({
75
+ $ref: "#/components/schemas/User",
76
+ });
77
+ expect(result).toBe("UserSchema");
78
+ });
79
+
80
+ it("should handle $ref with nullable", () => {
81
+ const result = convertSchemaToZodString({
82
+ $ref: "#/components/schemas/User",
83
+ nullable: true,
84
+ });
85
+ expect(result).toBe("z.union([UserSchema, z.null()])");
86
+ });
87
+
88
+ it("should handle invalid $ref format", () => {
89
+ const result = convertSchemaToZodString({
90
+ $ref: "invalid-ref",
91
+ });
92
+ expect(result).toBe("z.unknown()");
93
+ });
94
+
95
+ it("should handle $ref without match", () => {
96
+ const result = convertSchemaToZodString({
97
+ $ref: "#/invalid/path",
98
+ });
99
+ expect(result).toBe("z.unknown()");
100
+ });
101
+ });
102
+
103
+ describe("nullable handling", () => {
104
+ it("should add nullable modifier to string", () => {
105
+ const result = convertSchemaToZodString({
106
+ type: "string",
107
+ nullable: true,
108
+ });
109
+ expect(result).toBe("z.union([z.string(), z.null()])");
110
+ });
111
+
112
+ it("should add nullable modifier to number", () => {
113
+ const result = convertSchemaToZodString({
114
+ type: "number",
115
+ nullable: true,
116
+ });
117
+ expect(result).toBe("z.union([z.number(), z.null()])");
118
+ });
119
+
120
+ it("should add nullable modifier to array", () => {
121
+ const result = convertSchemaToZodString({
122
+ type: "array",
123
+ items: { type: "string" },
124
+ nullable: true,
125
+ });
126
+ expect(result).toBe("z.union([z.array(z.string()), z.null()])");
127
+ });
128
+
129
+ it("should not add nullable when false", () => {
130
+ const result = convertSchemaToZodString({
131
+ type: "string",
132
+ nullable: false,
133
+ });
134
+ expect(result).toBe("z.string()");
135
+ });
136
+
137
+ it("should add nullable modifier to union", () => {
138
+ const result = convertSchemaToZodString({
139
+ oneOf: [{ type: "string" }, { type: "number" }],
140
+ nullable: true,
141
+ });
142
+ expect(result).toBe("z.union([z.union([z.string(), z.number()]), z.null()])");
143
+ });
144
+
145
+ it("should add nullable modifier to intersection", () => {
146
+ const result = convertSchemaToZodString({
147
+ allOf: [{ type: "string" }, { type: "number" }],
148
+ nullable: true,
149
+ });
150
+ expect(result).toBe("z.union([z.intersection(z.string(), z.number()), z.null()])");
151
+ });
152
+
153
+ it("should add nullable modifier to object", () => {
154
+ const result = convertSchemaToZodString({
155
+ type: "object",
156
+ properties: {
157
+ name: { type: "string" },
158
+ },
159
+ required: ["name"],
160
+ nullable: true,
161
+ });
162
+ expect(result).toBe("z.union([z.object({ name: z.string() }), z.null()])");
163
+ });
164
+ });
165
+
166
+ describe("nested schemas", () => {
167
+ it("should handle nested arrays", () => {
168
+ const result = convertSchemaToZodString({
169
+ type: "array",
170
+ items: {
171
+ type: "array",
172
+ items: { type: "string" },
173
+ },
174
+ });
175
+ expect(result).toBe("z.array(z.array(z.string()))");
176
+ });
177
+
178
+ it("should handle nested objects", () => {
179
+ const result = convertSchemaToZodString({
180
+ type: "object",
181
+ properties: {
182
+ user: {
183
+ type: "object",
184
+ properties: {
185
+ name: { type: "string" },
186
+ },
187
+ required: ["name"],
188
+ },
189
+ },
190
+ required: ["user"],
191
+ });
192
+ expect(result).toBe(
193
+ "z.object({ user: z.object({ name: z.string() }) })",
194
+ );
195
+ });
196
+
197
+ it("should handle array of objects", () => {
198
+ const result = convertSchemaToZodString({
199
+ type: "array",
200
+ items: {
201
+ type: "object",
202
+ properties: {
203
+ name: { type: "string" },
204
+ },
205
+ required: ["name"],
206
+ },
207
+ });
208
+ expect(result).toBe("z.array(z.object({ name: z.string() }))");
209
+ });
210
+ });
211
+
212
+ describe("edge cases", () => {
213
+ it("should handle null schema", () => {
214
+ const result = convertSchemaToZodString(null);
215
+ expect(result).toBe("z.unknown()");
216
+ });
217
+
218
+ it("should handle undefined schema", () => {
219
+ const result = convertSchemaToZodString(undefined);
220
+ expect(result).toBe("z.unknown()");
221
+ });
222
+
223
+ it("should handle non-object schema", () => {
224
+ const result = convertSchemaToZodString("not-an-object");
225
+ expect(result).toBe("z.unknown()");
226
+ });
227
+
228
+ it("should handle object without type but with properties", () => {
229
+ const result = convertSchemaToZodString({
230
+ properties: {
231
+ name: { type: "string" },
232
+ },
233
+ required: ["name"],
234
+ });
235
+ expect(result).toBe("z.object({ name: z.string() })");
236
+ });
237
+
238
+ it("should handle unknown type", () => {
239
+ const result = convertSchemaToZodString({
240
+ type: "unknown-type",
241
+ });
242
+ expect(result).toBe("z.unknown()");
243
+ });
244
+ });
245
+
246
+ describe("priority handling", () => {
247
+ it("should prioritize oneOf over type", () => {
248
+ const result = convertSchemaToZodString({
249
+ type: "string",
250
+ oneOf: [{ type: "number" }],
251
+ });
252
+ expect(result).toBe("z.union([z.number()])");
253
+ });
254
+
255
+ it("should prioritize allOf over type", () => {
256
+ const result = convertSchemaToZodString({
257
+ type: "string",
258
+ allOf: [{ type: "number" }],
259
+ });
260
+ expect(result).toBe("z.number()");
261
+ });
262
+ });
263
+ });
264
+
265
+ describe("openApiToZodTsCode", () => {
266
+ beforeEach(() => {
267
+ clearZodSchemaToOpenApiSchemaRegistry();
268
+ });
269
+
270
+ afterEach(() => {
271
+ clearZodSchemaToOpenApiSchemaRegistry();
272
+ });
273
+
274
+ describe("basic OpenAPI conversion", () => {
275
+ it("should convert simple OpenAPI document with one schema", () => {
276
+ const openapi = {
277
+ components: {
278
+ schemas: {
279
+ User: {
280
+ type: "object",
281
+ properties: {
282
+ name: { type: "string" },
283
+ },
284
+ required: ["name"],
285
+ },
286
+ },
287
+ },
288
+ };
289
+
290
+ const result = openApiToZodTsCode(openapi);
291
+ expect(result).toContain("import { z } from 'zod';");
292
+ expect(result).toContain("export const UserSchema =");
293
+ expect(result).toContain("z.object({ name: z.string() })");
294
+ expect(result).toContain("export type User = z.infer<typeof UserSchema>;");
295
+ });
296
+
297
+ it("should convert OpenAPI document with multiple schemas", () => {
298
+ const openapi = {
299
+ components: {
300
+ schemas: {
301
+ User: {
302
+ type: "object",
303
+ properties: {
304
+ name: { type: "string" },
305
+ },
306
+ required: ["name"],
307
+ },
308
+ Product: {
309
+ type: "object",
310
+ properties: {
311
+ id: { type: "number" },
312
+ },
313
+ required: ["id"],
314
+ },
315
+ },
316
+ },
317
+ };
318
+
319
+ const result = openApiToZodTsCode(openapi);
320
+ expect(result).toContain("export const UserSchema =");
321
+ expect(result).toContain("export const ProductSchema =");
322
+ expect(result).toContain("export type User =");
323
+ expect(result).toContain("export type Product =");
324
+ });
325
+
326
+ it("should include file header comment", () => {
327
+ const openapi = {
328
+ components: {
329
+ schemas: {
330
+ User: { type: "string" },
331
+ },
332
+ },
333
+ };
334
+
335
+ const result = openApiToZodTsCode(openapi);
336
+ expect(result).toContain("This file was automatically generated");
337
+ expect(result).toContain("Do not manually edit this file");
338
+ });
339
+
340
+ it("should include required imports", () => {
341
+ const openapi = {
342
+ components: {
343
+ schemas: {
344
+ User: { type: "string" },
345
+ },
346
+ },
347
+ };
348
+
349
+ const result = openApiToZodTsCode(openapi);
350
+ expect(result).toContain("import { z } from 'zod';");
351
+ expect(result).toContain("import { ObjectId } from 'bson';");
352
+ expect(result).toContain("import { DateTime } from 'luxon';");
353
+ expect(result).toContain("import { LuxonDateSchema");
354
+ });
355
+ });
356
+
357
+ describe("schema dependencies", () => {
358
+ it("should sort schemas based on dependencies", () => {
359
+ const openapi = {
360
+ components: {
361
+ schemas: {
362
+ User: {
363
+ type: "object",
364
+ properties: {
365
+ profile: { $ref: "#/components/schemas/Profile" },
366
+ },
367
+ },
368
+ Profile: {
369
+ type: "object",
370
+ properties: {
371
+ name: { type: "string" },
372
+ },
373
+ required: ["name"],
374
+ },
375
+ },
376
+ },
377
+ };
378
+
379
+ const result = openApiToZodTsCode(openapi);
380
+ const profileIndex = result.indexOf("ProfileSchema");
381
+ const userIndex = result.indexOf("UserSchema");
382
+
383
+ expect(profileIndex).toBeLessThan(userIndex);
384
+ });
385
+
386
+ it("should handle circular dependencies gracefully", () => {
387
+ const openapi = {
388
+ components: {
389
+ schemas: {
390
+ A: {
391
+ type: "object",
392
+ properties: {
393
+ b: { $ref: "#/components/schemas/B" },
394
+ },
395
+ },
396
+ B: {
397
+ type: "object",
398
+ properties: {
399
+ a: { $ref: "#/components/schemas/A" },
400
+ },
401
+ },
402
+ },
403
+ },
404
+ };
405
+
406
+ const result = openApiToZodTsCode(openapi);
407
+ expect(result).toContain("ASchema");
408
+ expect(result).toContain("BSchema");
409
+ });
410
+ });
411
+
412
+ describe("edge cases", () => {
413
+ it("should handle OpenAPI document without components", () => {
414
+ const openapi = {};
415
+ const result = openApiToZodTsCode(openapi);
416
+ expect(result).toContain("import { z } from 'zod';");
417
+ expect(result).not.toContain("export const");
418
+ });
419
+
420
+ it("should handle OpenAPI document without schemas", () => {
421
+ const openapi = {
422
+ components: {},
423
+ };
424
+ const result = openApiToZodTsCode(openapi);
425
+ expect(result).toContain("import { z } from 'zod';");
426
+ expect(result).not.toContain("export const");
427
+ });
428
+
429
+ it("should handle empty schemas object", () => {
430
+ const openapi = {
431
+ components: {
432
+ schemas: {},
433
+ },
434
+ };
435
+ const result = openApiToZodTsCode(openapi);
436
+ expect(result).toContain("import { z } from 'zod';");
437
+ expect(result).not.toContain("export const");
438
+ });
439
+
440
+ it("should handle schema with all types", () => {
441
+ const openapi = {
442
+ components: {
443
+ schemas: {
444
+ StringSchema: { type: "string" },
445
+ NumberSchema: { type: "number" },
446
+ IntegerSchema: { type: "integer" },
447
+ BooleanSchema: { type: "boolean" },
448
+ ArraySchema: {
449
+ type: "array",
450
+ items: { type: "string" },
451
+ },
452
+ ObjectSchema: {
453
+ type: "object",
454
+ properties: {
455
+ name: { type: "string" },
456
+ },
457
+ },
458
+ UnionSchema: {
459
+ oneOf: [{ type: "string" }, { type: "number" }],
460
+ },
461
+ IntersectionSchema: {
462
+ allOf: [{ type: "string" }, { type: "number" }],
463
+ },
464
+ },
465
+ },
466
+ };
467
+
468
+ const result = openApiToZodTsCode(openapi);
469
+ expect(result).toContain("StringSchema");
470
+ expect(result).toContain("NumberSchema");
471
+ expect(result).toContain("IntegerSchema");
472
+ expect(result).toContain("BooleanSchema");
473
+ expect(result).toContain("ArraySchema");
474
+ expect(result).toContain("ObjectSchema");
475
+ expect(result).toContain("UnionSchema");
476
+ expect(result).toContain("IntersectionSchema");
477
+ });
478
+ });
479
+ });
480
+
package/src/to-zod.ts ADDED
@@ -0,0 +1,112 @@
1
+ import { convertOpenAPIBooleanToZod } from "./types/boolean";
2
+ import { convertOpenAPINumberToZod } from "./types/number";
3
+ import { convertOpenAPIStringToZod } from "./types/string";
4
+ import { convertOpenAPIArrayToZod } from "./types/array";
5
+ import { convertOpenAPIObjectToZod } from "./types/object";
6
+ import { convertOpenAPIUnionToZod } from "./types/union";
7
+ import { convertOpenAPIIntersectionToZod } from "./types/intersection";
8
+ import type { AnySchema } from "./types/types";
9
+
10
+ export function convertSchemaToZodString(schema: AnySchema): string {
11
+ if (!schema || typeof schema !== "object") return "z.unknown()";
12
+
13
+ if (schema["$ref"] && typeof schema["$ref"] === "string") {
14
+ const match = (schema["$ref"] as string).match(
15
+ /#\/components\/schemas\/(.+)/,
16
+ );
17
+ let result = "z.unknown()";
18
+ if (match && match[1]) {
19
+ result = `${match[1]}Schema`;
20
+ }
21
+ if (schema["nullable"] === true) {
22
+ result = `z.union([${result}, z.null()])`;
23
+ }
24
+ return result;
25
+ }
26
+ let result: string = "z.unknown()";
27
+
28
+ if ("oneOf" in schema && Array.isArray(schema["oneOf"])) {
29
+ result = convertOpenAPIUnionToZod(
30
+ schema as { oneOf: AnySchema[] },
31
+ convertSchemaToZodString,
32
+ );
33
+ } else if ("allOf" in schema && Array.isArray(schema["allOf"])) {
34
+ result = convertOpenAPIIntersectionToZod(
35
+ schema as { allOf: AnySchema[] },
36
+ convertSchemaToZodString,
37
+ );
38
+ } else {
39
+ switch (schema["type"]) {
40
+ case "string":
41
+ result = convertOpenAPIStringToZod({
42
+ enum: schema["enum"],
43
+ format: schema["format"],
44
+ maxLength: schema["maxLength"],
45
+ minLength: schema["minLength"],
46
+ pattern: schema["pattern"],
47
+ type: "string",
48
+ });
49
+ break;
50
+ case "number":
51
+ result = convertOpenAPINumberToZod({
52
+ maximum: schema["maximum"],
53
+ minimum: schema["minimum"],
54
+ type: "number",
55
+ });
56
+ break;
57
+ case "integer":
58
+ result = convertOpenAPINumberToZod({
59
+ maximum: schema["maximum"],
60
+ minimum: schema["minimum"],
61
+ type: "integer",
62
+ });
63
+ break;
64
+ case "boolean":
65
+ result = convertOpenAPIBooleanToZod({ type: "boolean" });
66
+ break;
67
+ case "array":
68
+ result = convertOpenAPIArrayToZod(
69
+ {
70
+ items: schema["items"],
71
+ maxItems: schema["maxItems"],
72
+ minItems: schema["minItems"],
73
+ type: "array",
74
+ },
75
+ convertSchemaToZodString,
76
+ );
77
+ break;
78
+ case "object":
79
+ result = convertOpenAPIObjectToZod(
80
+ {
81
+ additionalProperties: schema["additionalProperties"],
82
+ properties: schema["properties"],
83
+ required: schema["required"],
84
+ type: "object",
85
+ },
86
+ convertSchemaToZodString,
87
+ );
88
+ break;
89
+ default:
90
+ if (schema["properties"]) {
91
+ result = convertOpenAPIObjectToZod(
92
+ {
93
+ additionalProperties: schema["additionalProperties"],
94
+ properties: schema["properties"],
95
+ required: schema["required"],
96
+ type: "object",
97
+ },
98
+ convertSchemaToZodString,
99
+ );
100
+ } else {
101
+ result = "z.unknown()";
102
+ }
103
+ break;
104
+ }
105
+ }
106
+
107
+ if (schema["nullable"] === true) {
108
+ result = `z.union([${result}, z.null()])`;
109
+ }
110
+
111
+ return result;
112
+ }