-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathjson-schema-to-zod.ts
More file actions
94 lines (85 loc) · 2.85 KB
/
json-schema-to-zod.ts
File metadata and controls
94 lines (85 loc) · 2.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import type { OpenAPIV3_1 as OpenAPI } from 'openapi-types';
export interface ZodResult {
zodString: string;
imports: Set<string>;
}
export function convertSchema(schema: OpenAPI.SchemaObject | OpenAPI.ReferenceObject): ZodResult {
const imports = new Set<string>();
function walk(s: OpenAPI.SchemaObject | OpenAPI.ReferenceObject): string {
if ('$ref' in s) {
const match = s.$ref.match(/^#\/components\/schemas\/(.+)$/);
const name = match?.[1] ?? s.$ref;
imports.add(name);
return name;
}
if ('allOf' in s && Array.isArray(s.allOf)) {
const items = s.allOf as Array<OpenAPI.SchemaObject | OpenAPI.ReferenceObject>;
if (items.length === 0) {
return 'z.any()';
}
let expr = walk(items[0]!);
for (const sub of items.slice(1)) {
expr = `z.intersection(${expr}, ${walk(sub)})`;
}
return expr;
}
if ('oneOf' in s && Array.isArray(s.oneOf)) {
const items = s.oneOf as Array<OpenAPI.SchemaObject | OpenAPI.ReferenceObject>;
if (items.length === 0) {
return 'z.any()';
}
if (items.length === 1) {
return walk(items[0]!);
}
const parts = items.map((sub) => walk(sub)).join(', ');
return `z.union([${parts}])`;
}
if ('anyOf' in s && Array.isArray(s.anyOf)) {
const items = s.anyOf as Array<OpenAPI.SchemaObject | OpenAPI.ReferenceObject>;
if (items.length === 0) {
return 'z.any()';
}
if (items.length === 1) {
return walk(items[0]!);
}
const parts = items.map((sub) => walk(sub)).join(', ');
return `z.union([${parts}])`;
}
if (s.enum) {
const values = s.enum.map((v) => JSON.stringify(v)).join(', ');
return `z.enum([${values}])`;
}
switch (s.type) {
case 'string':
return 'z.string()';
case 'number':
case 'integer':
return 'z.number()';
case 'boolean':
return 'z.boolean()';
case 'array':
if (!s.items) return 'z.array(z.any())';
return `z.array(${walk(s.items)})`;
case 'object':
default: {
const props = s.properties ?? {};
const required = new Set(s.required ?? []);
const fields = Object.entries(props).map(([key, value]) => {
let expr = walk(value as any);
if (!required.has(key)) {
expr += '.optional()';
}
const desc = (value as any).description;
const meta = desc ? `.meta({ description: ${JSON.stringify(desc.replace(/\n/g, ' '))} })` : '';
return `${key}: ${expr}${meta}`;
});
const inner = fields.map((f) => ` ${f}`).join(',\n');
return `z.object({\n${inner}\n})`;
}
}
}
const zodString = walk(schema);
imports.delete('');
imports.delete((schema as any).title as string); // not needed
return { zodString, imports };
}