-
-
Notifications
You must be signed in to change notification settings - Fork 254
Expand file tree
/
Copy pathjsonschema-to-zod.ts
More file actions
113 lines (101 loc) · 3.7 KB
/
jsonschema-to-zod.ts
File metadata and controls
113 lines (101 loc) · 3.7 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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import * as z from 'zod';
type JsonSchemaEnumValue = string | number | boolean | null;
type JsonSchema = {
type?: string | string[];
description?: string;
default?: unknown;
enum?: unknown[];
items?: JsonSchema;
properties?: Record<string, JsonSchema>;
required?: string[];
};
function applyDescription<T extends z.ZodTypeAny>(schema: T, description?: string): T {
if (!description) return schema;
return schema.describe(description) as T;
}
function applyDefault(schema: z.ZodTypeAny, defaultValue: unknown): z.ZodTypeAny {
if (defaultValue === undefined) return schema;
return schema.default(defaultValue);
}
function isObjectSchema(schema: JsonSchema): boolean {
const types =
schema.type === undefined ? [] : Array.isArray(schema.type) ? schema.type : [schema.type];
return types.includes('object') || schema.properties !== undefined;
}
function isEnumValue(value: unknown): value is JsonSchemaEnumValue {
return (
value === null ||
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean'
);
}
export function jsonSchemaToZod(schema: JsonSchema | unknown): z.ZodTypeAny {
if (!schema || typeof schema !== 'object') {
return z.any();
}
const s = schema as JsonSchema;
if (Array.isArray(s.enum)) {
const enumValues = s.enum.filter(isEnumValue);
if (enumValues.length === 0) {
return applyDescription(z.any(), s.description);
}
const allStrings = enumValues.every((v) => typeof v === 'string');
if (allStrings) {
const stringValues = enumValues as string[];
if (stringValues.length === 1) {
return applyDescription(z.literal(stringValues[0]), s.description);
}
return applyDescription(z.enum(stringValues as [string, ...string[]]), s.description);
}
// z.enum only supports string unions; use z.literal union for mixed enums.
const literals = enumValues.map((v) => z.literal(v)) as z.ZodLiteral<JsonSchemaEnumValue>[];
if (literals.length === 1) {
return applyDescription(literals[0], s.description);
}
return applyDescription(
z.union(
literals as [
z.ZodLiteral<JsonSchemaEnumValue>,
z.ZodLiteral<JsonSchemaEnumValue>,
...z.ZodLiteral<JsonSchemaEnumValue>[],
],
),
s.description,
);
}
const types = s.type === undefined ? [] : Array.isArray(s.type) ? s.type : [s.type];
const primaryType = types[0];
switch (primaryType) {
case 'string':
return applyDefault(applyDescription(z.string(), s.description), s.default);
case 'integer':
return applyDefault(applyDescription(z.number().int(), s.description), s.default);
case 'number':
return applyDefault(applyDescription(z.number(), s.description), s.default);
case 'boolean':
return applyDefault(applyDescription(z.boolean(), s.description), s.default);
case 'array': {
const itemSchema = jsonSchemaToZod(s.items ?? {});
return applyDefault(applyDescription(z.array(itemSchema), s.description), s.default);
}
case 'object':
default: {
if (!isObjectSchema(s)) {
return applyDefault(applyDescription(z.any(), s.description), s.default);
}
const required = new Set(s.required ?? []);
const props = s.properties ?? {};
const shape: Record<string, z.ZodTypeAny> = {};
for (const [key, value] of Object.entries(props)) {
const propSchema = jsonSchemaToZod(value);
shape[key] = required.has(key) ? propSchema : propSchema.optional();
}
// Use passthrough to avoid breaking when Apple adds new fields.
return applyDefault(
applyDescription(z.object(shape).passthrough(), s.description),
s.default,
);
}
}
}