@signe/schema-to-zod 2.8.3 → 2.9.0
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/dist/index.d.ts +8 -2
- package/dist/index.js +325 -58
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/readme.md +46 -0
- package/src/index.ts +442 -84
- package/tests/validate.spec.ts +586 -2
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -92,6 +92,52 @@ Parameters:
|
|
|
92
92
|
Returns:
|
|
93
93
|
- A record of Zod type definitions that can be used with `z.object()`
|
|
94
94
|
|
|
95
|
+
### `jsonSchemaToZodSchema(schema: JSONSchema7 | JSONSchema7Definition[]): ZodType<unknown>`
|
|
96
|
+
|
|
97
|
+
Converts a JSON Schema to a full Zod schema.
|
|
98
|
+
|
|
99
|
+
Use this API when the schema relies on:
|
|
100
|
+
- `anyOf`
|
|
101
|
+
- `not`
|
|
102
|
+
- `allOf`
|
|
103
|
+
- `dependentRequired`
|
|
104
|
+
- `dependentSchemas`
|
|
105
|
+
- `if / then / else`
|
|
106
|
+
- conditional branches that must validate only the active fields
|
|
107
|
+
|
|
108
|
+
Example:
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { jsonSchemaToZodSchema } from '@signe/schema-to-zod';
|
|
112
|
+
|
|
113
|
+
const schema = {
|
|
114
|
+
type: 'object',
|
|
115
|
+
properties: {
|
|
116
|
+
name: { type: 'string' },
|
|
117
|
+
itemType: { type: 'string', enum: ['item', 'weapon', 'armor'] }
|
|
118
|
+
},
|
|
119
|
+
required: ['name', 'itemType'],
|
|
120
|
+
allOf: [
|
|
121
|
+
{
|
|
122
|
+
if: {
|
|
123
|
+
properties: {
|
|
124
|
+
itemType: { const: 'weapon' }
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
then: {
|
|
128
|
+
properties: {
|
|
129
|
+
atk: { type: 'number' },
|
|
130
|
+
weaponType: { type: 'string' }
|
|
131
|
+
},
|
|
132
|
+
required: ['atk', 'weaponType']
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const zodSchema = jsonSchemaToZodSchema(schema);
|
|
139
|
+
```
|
|
140
|
+
|
|
95
141
|
## Examples
|
|
96
142
|
|
|
97
143
|
### Nested Objects
|
package/src/index.ts
CHANGED
|
@@ -1,18 +1,73 @@
|
|
|
1
|
-
import { z,
|
|
1
|
+
import { z, type RefinementCtx, type ZodRawShape, type ZodType } from "zod";
|
|
2
2
|
import type { JSONSchema7, JSONSchema7Definition, JSONSchema7TypeName } from "json-schema";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Standard JSON Schema types only
|
|
6
6
|
*/
|
|
7
|
-
type SchemaType = Exclude<JSONSchema7TypeName,
|
|
7
|
+
type SchemaType = Exclude<JSONSchema7TypeName, "array" | "object"> | "array" | "object";
|
|
8
|
+
type ExtendedJSONSchema7 = JSONSchema7 & {
|
|
9
|
+
dependentRequired?: Record<string, string[]>;
|
|
10
|
+
dependentSchemas?: Record<string, JSONSchema7Definition>;
|
|
11
|
+
};
|
|
8
12
|
|
|
9
13
|
function isValidSchemaType(type: unknown): type is SchemaType {
|
|
10
|
-
if (typeof type !==
|
|
11
|
-
return [
|
|
14
|
+
if (typeof type !== "string") return false;
|
|
15
|
+
return ["string", "number", "boolean", "integer", "array", "object", "null"].includes(type);
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
function isJSONSchema7(schema: JSONSchema7Definition): schema is JSONSchema7 {
|
|
15
|
-
return typeof schema !==
|
|
19
|
+
return typeof schema !== "boolean";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function asExtendedJSONSchema7(schema: JSONSchema7): ExtendedJSONSchema7 {
|
|
23
|
+
return schema as ExtendedJSONSchema7;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function unwrapSchemaDefinition(schema: unknown): JSONSchema7Definition | null {
|
|
27
|
+
if (typeof schema === "boolean") {
|
|
28
|
+
return schema;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!schema || typeof schema !== "object") {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if ("schema" in schema) {
|
|
36
|
+
return unwrapSchemaDefinition((schema as { schema?: unknown }).schema);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return schema as JSONSchema7Definition;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
43
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function areValuesEqual(left: unknown, right: unknown): boolean {
|
|
47
|
+
if (Object.is(left, right)) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
52
|
+
if (left.length !== right.length) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return left.every((item, index) => areValuesEqual(item, right[index]));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (isPlainObject(left) && isPlainObject(right)) {
|
|
60
|
+
const leftKeys = Object.keys(left);
|
|
61
|
+
const rightKeys = Object.keys(right);
|
|
62
|
+
|
|
63
|
+
if (leftKeys.length !== rightKeys.length) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return leftKeys.every((key) => areValuesEqual(left[key], right[key]));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return false;
|
|
16
71
|
}
|
|
17
72
|
|
|
18
73
|
/**
|
|
@@ -24,66 +79,60 @@ const percent = z.number().min(0).max(100);
|
|
|
24
79
|
* Map of JSON Schema formats to their corresponding Zod schema creators
|
|
25
80
|
*/
|
|
26
81
|
const formatMap: Record<string, (schema: JSONSchema7) => ZodType<unknown>> = {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
82
|
+
"date": () => z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
|
83
|
+
"date-time": () => z.string().datetime(),
|
|
84
|
+
"email": () => z.string().email(),
|
|
85
|
+
"hostname": () => z.string(),
|
|
86
|
+
"ipv4": () => z.string().ip({ version: "v4" }),
|
|
87
|
+
"ipv6": () => z.string().ip({ version: "v6" }),
|
|
88
|
+
"uri": () => z.string().url(),
|
|
89
|
+
"uuid": () => z.string().uuid(),
|
|
90
|
+
"color": () => z.string().regex(/^#(?:[0-9a-fA-F]{3}){1,2}$/),
|
|
91
|
+
"password": () => z.string(),
|
|
92
|
+
"code": () => z.string(),
|
|
93
|
+
"percent": () => percent,
|
|
39
94
|
};
|
|
40
95
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const typeMap: Record<SchemaType, (schema: JSONSchema7) => ZodType<unknown, ZodTypeDef, unknown>> = {
|
|
45
|
-
'string': (schema) => {
|
|
46
|
-
if (schema.format && formatMap[schema.format]) {
|
|
47
|
-
return formatMap[schema.format](schema);
|
|
48
|
-
}
|
|
49
|
-
return z.string();
|
|
50
|
-
},
|
|
51
|
-
'number': (schema) => {
|
|
52
|
-
if (schema.format === 'percent') {
|
|
53
|
-
return percent;
|
|
54
|
-
}
|
|
55
|
-
return z.number();
|
|
56
|
-
},
|
|
57
|
-
'integer': () => z.number().int(),
|
|
58
|
-
'boolean': () => z.boolean(),
|
|
59
|
-
'null': () => z.null(),
|
|
60
|
-
'array': (schema: JSONSchema7) => {
|
|
61
|
-
if (!schema.items || Array.isArray(schema.items)) {
|
|
62
|
-
throw new Error('Invalid array items');
|
|
63
|
-
}
|
|
96
|
+
function createRequiredUnknownSchema(): ZodType<unknown> {
|
|
97
|
+
return z.custom<unknown>((value) => value !== undefined);
|
|
98
|
+
}
|
|
64
99
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
100
|
+
function inferSchemaType(schema: JSONSchema7): SchemaType | null {
|
|
101
|
+
if (schema.type !== undefined && !isValidSchemaType(schema.type)) {
|
|
102
|
+
throw new Error(`Unsupported type: ${schema.type}`);
|
|
103
|
+
}
|
|
68
104
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
105
|
+
if (schema.type && isValidSchemaType(schema.type)) {
|
|
106
|
+
return schema.type;
|
|
107
|
+
}
|
|
72
108
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
109
|
+
if (schema.properties || schema.required || schema.if || schema.then || schema.else || schema.allOf) {
|
|
110
|
+
return "object";
|
|
111
|
+
}
|
|
77
112
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
'object': (schema: JSONSchema7) => {
|
|
81
|
-
if (!schema.properties) {
|
|
82
|
-
throw new Error('Invalid object schema: missing properties');
|
|
83
|
-
}
|
|
84
|
-
return z.object(jsonSchemaToZod(schema));
|
|
113
|
+
if (schema.items) {
|
|
114
|
+
return "array";
|
|
85
115
|
}
|
|
86
|
-
|
|
116
|
+
|
|
117
|
+
if (
|
|
118
|
+
schema.pattern ||
|
|
119
|
+
schema.minLength !== undefined ||
|
|
120
|
+
schema.maxLength !== undefined ||
|
|
121
|
+
(schema.format && schema.format !== "percent")
|
|
122
|
+
) {
|
|
123
|
+
return "string";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (
|
|
127
|
+
schema.minimum !== undefined ||
|
|
128
|
+
schema.maximum !== undefined ||
|
|
129
|
+
schema.format === "percent"
|
|
130
|
+
) {
|
|
131
|
+
return "number";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
87
136
|
|
|
88
137
|
/**
|
|
89
138
|
* Applies validators to a Zod schema based on JSON Schema property constraints
|
|
@@ -96,7 +145,7 @@ function applyValidators(
|
|
|
96
145
|
let zodTypeWithValidators = zodType;
|
|
97
146
|
|
|
98
147
|
// String validators
|
|
99
|
-
if (schema.type ===
|
|
148
|
+
if (schema.type === "string" && required) {
|
|
100
149
|
zodTypeWithValidators = (zodTypeWithValidators as z.ZodString).min(1);
|
|
101
150
|
|
|
102
151
|
if (schema.minLength !== undefined) {
|
|
@@ -109,7 +158,7 @@ function applyValidators(
|
|
|
109
158
|
}
|
|
110
159
|
|
|
111
160
|
// Array validators
|
|
112
|
-
if (schema.type ===
|
|
161
|
+
if (schema.type === "array") {
|
|
113
162
|
if (schema.minItems !== undefined) {
|
|
114
163
|
zodTypeWithValidators = (zodTypeWithValidators as z.ZodArray<ZodType>).min(schema.minItems);
|
|
115
164
|
}
|
|
@@ -119,7 +168,7 @@ function applyValidators(
|
|
|
119
168
|
}
|
|
120
169
|
|
|
121
170
|
// Number validators
|
|
122
|
-
if (schema.type ===
|
|
171
|
+
if (schema.type === "number" || schema.type === "integer") {
|
|
123
172
|
if (schema.minimum !== undefined) {
|
|
124
173
|
zodTypeWithValidators = (zodTypeWithValidators as z.ZodNumber).min(schema.minimum);
|
|
125
174
|
}
|
|
@@ -129,29 +178,318 @@ function applyValidators(
|
|
|
129
178
|
}
|
|
130
179
|
}
|
|
131
180
|
|
|
181
|
+
// Pattern validators
|
|
182
|
+
if (schema.pattern) {
|
|
183
|
+
zodTypeWithValidators = (zodTypeWithValidators as z.ZodString).regex(new RegExp(schema.pattern));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Const validator
|
|
187
|
+
if (schema.const !== undefined) {
|
|
188
|
+
zodTypeWithValidators = zodTypeWithValidators.refine(
|
|
189
|
+
(value) => areValuesEqual(value, schema.const),
|
|
190
|
+
{
|
|
191
|
+
message: `Must be equal to: ${JSON.stringify(schema.const)}`,
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
132
196
|
// Enum validators
|
|
133
197
|
if (schema.enum) {
|
|
134
198
|
zodTypeWithValidators = zodTypeWithValidators.refine(
|
|
135
|
-
(value): value is typeof schema.enum[number] =>
|
|
199
|
+
(value): value is typeof schema.enum[number] =>
|
|
200
|
+
schema.enum!.some((enumValue) => areValuesEqual(value, enumValue)),
|
|
136
201
|
{
|
|
137
|
-
message: `Must be one of: ${schema.enum.join(", ")}`,
|
|
202
|
+
message: `Must be one of: ${schema.enum.map((value) => JSON.stringify(value)).join(", ")}`,
|
|
138
203
|
}
|
|
139
204
|
);
|
|
140
205
|
}
|
|
141
206
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
207
|
+
return zodTypeWithValidators;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function applyLogicalCompositionValidators(
|
|
211
|
+
zodType: ZodType<unknown>,
|
|
212
|
+
schema: JSONSchema7
|
|
213
|
+
): ZodType<unknown> {
|
|
214
|
+
let composedType = zodType;
|
|
215
|
+
|
|
216
|
+
if (schema.anyOf && schema.anyOf.length > 0) {
|
|
217
|
+
composedType = composedType.superRefine((value, context) => {
|
|
218
|
+
const matchesAnySchema = schema.anyOf!.some((subSchema) => matchesSchemaDefinition(subSchema, value));
|
|
219
|
+
|
|
220
|
+
if (!matchesAnySchema) {
|
|
221
|
+
context.addIssue({
|
|
222
|
+
code: z.ZodIssueCode.custom,
|
|
223
|
+
message: "Must match at least one schema in anyOf",
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
});
|
|
145
227
|
}
|
|
146
228
|
|
|
147
|
-
|
|
229
|
+
if (schema.not !== undefined) {
|
|
230
|
+
composedType = composedType.superRefine((value, context) => {
|
|
231
|
+
if (matchesSchemaDefinition(schema.not!, value)) {
|
|
232
|
+
context.addIssue({
|
|
233
|
+
code: z.ZodIssueCode.custom,
|
|
234
|
+
message: "Must not match schema in not",
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return composedType;
|
|
148
241
|
}
|
|
149
242
|
|
|
150
|
-
function
|
|
151
|
-
if (!
|
|
152
|
-
|
|
243
|
+
function matchesSchemaDefinition(schema: JSONSchema7Definition, value: unknown): boolean {
|
|
244
|
+
if (!isJSONSchema7(schema)) {
|
|
245
|
+
return schema;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return jsonSchemaDefinitionToZod(schema).safeParse(value).success;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function addSchemaIssues(
|
|
252
|
+
schema: JSONSchema7Definition,
|
|
253
|
+
value: unknown,
|
|
254
|
+
context: RefinementCtx
|
|
255
|
+
): void {
|
|
256
|
+
if (!isJSONSchema7(schema)) {
|
|
257
|
+
if (!schema) {
|
|
258
|
+
context.addIssue({
|
|
259
|
+
code: z.ZodIssueCode.custom,
|
|
260
|
+
message: "Schema condition rejected the current value",
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const result = jsonSchemaDefinitionToZod(schema).safeParse(value);
|
|
267
|
+
|
|
268
|
+
if (result.success) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for (const issue of result.error.issues) {
|
|
273
|
+
context.addIssue(issue);
|
|
153
274
|
}
|
|
154
|
-
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function collectComposablePropertyKeys(schema: JSONSchema7): Set<string> {
|
|
278
|
+
const extendedSchema = asExtendedJSONSchema7(schema);
|
|
279
|
+
const keys = new Set<string>();
|
|
280
|
+
|
|
281
|
+
const visit = (definition?: JSONSchema7Definition): void => {
|
|
282
|
+
if (!definition || !isJSONSchema7(definition)) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const extendedDefinition = asExtendedJSONSchema7(definition);
|
|
287
|
+
|
|
288
|
+
if (extendedDefinition.properties) {
|
|
289
|
+
for (const key of Object.keys(extendedDefinition.properties)) {
|
|
290
|
+
keys.add(key);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (extendedDefinition.required) {
|
|
295
|
+
for (const key of extendedDefinition.required) {
|
|
296
|
+
keys.add(key);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (extendedDefinition.dependentRequired) {
|
|
301
|
+
for (const [key, requiredKeys] of Object.entries(extendedDefinition.dependentRequired)) {
|
|
302
|
+
keys.add(key);
|
|
303
|
+
|
|
304
|
+
for (const requiredKey of requiredKeys) {
|
|
305
|
+
keys.add(requiredKey);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (extendedDefinition.dependentSchemas) {
|
|
311
|
+
for (const [key, dependentSchema] of Object.entries(extendedDefinition.dependentSchemas)) {
|
|
312
|
+
keys.add(key);
|
|
313
|
+
visit(dependentSchema);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (extendedDefinition.anyOf) {
|
|
318
|
+
for (const item of extendedDefinition.anyOf) {
|
|
319
|
+
visit(item);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (extendedDefinition.allOf) {
|
|
324
|
+
for (const item of extendedDefinition.allOf) {
|
|
325
|
+
visit(item);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
visit(extendedDefinition.not);
|
|
330
|
+
visit(extendedDefinition.if);
|
|
331
|
+
visit(extendedDefinition.then);
|
|
332
|
+
visit(extendedDefinition.else);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
visit(extendedSchema);
|
|
336
|
+
|
|
337
|
+
return keys;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function hasComposableValidation(schema: JSONSchema7): boolean {
|
|
341
|
+
const extendedSchema = asExtendedJSONSchema7(schema);
|
|
342
|
+
return Boolean(
|
|
343
|
+
extendedSchema.dependentRequired ||
|
|
344
|
+
extendedSchema.dependentSchemas ||
|
|
345
|
+
extendedSchema.if ||
|
|
346
|
+
extendedSchema.then ||
|
|
347
|
+
extendedSchema.else ||
|
|
348
|
+
(Array.isArray(extendedSchema.allOf) && extendedSchema.allOf.length > 0)
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function createPrimitiveType(schema: JSONSchema7, type: Exclude<SchemaType, "array" | "object">): ZodType<unknown> {
|
|
353
|
+
switch (type) {
|
|
354
|
+
case "string":
|
|
355
|
+
if (schema.format && formatMap[schema.format]) {
|
|
356
|
+
return formatMap[schema.format](schema);
|
|
357
|
+
}
|
|
358
|
+
return z.string();
|
|
359
|
+
case "number":
|
|
360
|
+
if (schema.format === "percent") {
|
|
361
|
+
return percent;
|
|
362
|
+
}
|
|
363
|
+
return z.number();
|
|
364
|
+
case "integer":
|
|
365
|
+
return z.number().int();
|
|
366
|
+
case "boolean":
|
|
367
|
+
return z.boolean();
|
|
368
|
+
case "null":
|
|
369
|
+
return z.null();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function buildObjectSchema(schema: JSONSchema7): ZodType<unknown> {
|
|
374
|
+
const extendedSchema = asExtendedJSONSchema7(schema);
|
|
375
|
+
const shape: ZodRawShape = {};
|
|
376
|
+
|
|
377
|
+
if (extendedSchema.properties) {
|
|
378
|
+
for (const [key, propertySchema] of Object.entries(extendedSchema.properties)) {
|
|
379
|
+
if (!isJSONSchema7(propertySchema)) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
shape[key] = convertPropertyToZod(propertySchema, key, extendedSchema);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (extendedSchema.required) {
|
|
388
|
+
for (const key of extendedSchema.required) {
|
|
389
|
+
if (!shape[key]) {
|
|
390
|
+
shape[key] = createRequiredUnknownSchema();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
for (const key of collectComposablePropertyKeys(extendedSchema)) {
|
|
396
|
+
if (!shape[key]) {
|
|
397
|
+
shape[key] = z.unknown().optional();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const baseObject = z.object(shape);
|
|
402
|
+
|
|
403
|
+
if (!hasComposableValidation(extendedSchema)) {
|
|
404
|
+
return baseObject;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return baseObject.superRefine((value, context) => {
|
|
408
|
+
if (extendedSchema.dependentRequired) {
|
|
409
|
+
for (const [triggerKey, requiredKeys] of Object.entries(extendedSchema.dependentRequired)) {
|
|
410
|
+
if (!Object.prototype.hasOwnProperty.call(value, triggerKey)) {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
for (const requiredKey of requiredKeys) {
|
|
415
|
+
if (!Object.prototype.hasOwnProperty.call(value, requiredKey)) {
|
|
416
|
+
context.addIssue({
|
|
417
|
+
code: z.ZodIssueCode.custom,
|
|
418
|
+
message: `Property "${requiredKey}" is required when "${triggerKey}" is present`,
|
|
419
|
+
path: [requiredKey],
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (extendedSchema.dependentSchemas) {
|
|
427
|
+
for (const [triggerKey, dependentSchema] of Object.entries(extendedSchema.dependentSchemas)) {
|
|
428
|
+
if (!Object.prototype.hasOwnProperty.call(value, triggerKey)) {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
addSchemaIssues(dependentSchema, value, context);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (extendedSchema.if) {
|
|
437
|
+
const matches = matchesSchemaDefinition(extendedSchema.if, value);
|
|
438
|
+
|
|
439
|
+
if (matches && extendedSchema.then) {
|
|
440
|
+
addSchemaIssues(extendedSchema.then, value, context);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (!matches && extendedSchema.else) {
|
|
444
|
+
addSchemaIssues(extendedSchema.else, value, context);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (extendedSchema.allOf) {
|
|
449
|
+
for (const item of extendedSchema.allOf) {
|
|
450
|
+
addSchemaIssues(item, value, context);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function buildArraySchema(schema: JSONSchema7): ZodType<unknown> {
|
|
457
|
+
if (!schema.items || Array.isArray(schema.items)) {
|
|
458
|
+
throw new Error("Invalid array items");
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return applyValidators(z.array(jsonSchemaDefinitionToZod(schema.items, true)), schema, true);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function jsonSchemaDefinitionToZod(schema: JSONSchema7Definition, required = true): ZodType<unknown> {
|
|
465
|
+
if (!isJSONSchema7(schema)) {
|
|
466
|
+
return schema ? z.any() : z.never();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const inferredType = inferSchemaType(schema);
|
|
470
|
+
let zodType: ZodType<unknown>;
|
|
471
|
+
|
|
472
|
+
if (inferredType === "object") {
|
|
473
|
+
zodType = buildObjectSchema(schema);
|
|
474
|
+
return applyLogicalCompositionValidators(zodType, schema);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (inferredType === "array") {
|
|
478
|
+
zodType = buildArraySchema(schema);
|
|
479
|
+
return applyLogicalCompositionValidators(zodType, schema);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (inferredType) {
|
|
483
|
+
zodType = applyValidators(
|
|
484
|
+
createPrimitiveType(schema, inferredType as Exclude<SchemaType, "array" | "object">),
|
|
485
|
+
schema,
|
|
486
|
+
required
|
|
487
|
+
);
|
|
488
|
+
return applyLogicalCompositionValidators(zodType, schema);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
zodType = applyValidators(z.any(), schema, required);
|
|
492
|
+
return applyLogicalCompositionValidators(zodType, schema);
|
|
155
493
|
}
|
|
156
494
|
|
|
157
495
|
/**
|
|
@@ -163,18 +501,35 @@ function convertPropertyToZod(
|
|
|
163
501
|
parentSchema: JSONSchema7
|
|
164
502
|
): ZodType<unknown> {
|
|
165
503
|
const required = Array.isArray(parentSchema.required) && parentSchema.required.includes(key);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
504
|
+
const propertySchema = jsonSchemaDefinitionToZod(schema, required);
|
|
505
|
+
|
|
506
|
+
return required ? propertySchema : propertySchema.optional();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Converts a JSON Schema to a full Zod schema.
|
|
511
|
+
* This export should be preferred when the schema uses allOf or if/then/else.
|
|
512
|
+
*/
|
|
513
|
+
export function jsonSchemaToZodSchema(
|
|
514
|
+
schema: JSONSchema7 | JSONSchema7Definition[],
|
|
515
|
+
): ZodType<unknown> {
|
|
516
|
+
if (Array.isArray(schema)) {
|
|
517
|
+
const definitions = schema
|
|
518
|
+
.map(unwrapSchemaDefinition)
|
|
519
|
+
.filter((definition): definition is JSONSchema7Definition => definition !== null);
|
|
520
|
+
|
|
521
|
+
return jsonSchemaDefinitionToZod({
|
|
522
|
+
type: "object",
|
|
523
|
+
allOf: definitions,
|
|
524
|
+
});
|
|
170
525
|
}
|
|
171
526
|
|
|
172
|
-
|
|
173
|
-
return required ? typeValidator : typeValidator.optional();
|
|
527
|
+
return jsonSchemaDefinitionToZod(schema);
|
|
174
528
|
}
|
|
175
529
|
|
|
176
530
|
/**
|
|
177
|
-
* Converts a JSON Schema to a Zod
|
|
531
|
+
* Converts a JSON Schema to a Zod shape.
|
|
532
|
+
* This is kept for compatibility with the previous API.
|
|
178
533
|
*/
|
|
179
534
|
export function jsonSchemaToZod(
|
|
180
535
|
schema: JSONSchema7 | JSONSchema7Definition[],
|
|
@@ -182,13 +537,16 @@ export function jsonSchemaToZod(
|
|
|
182
537
|
const zodSchema: Record<string, ZodType<unknown>> = {};
|
|
183
538
|
|
|
184
539
|
if (Array.isArray(schema)) {
|
|
185
|
-
// Handle array of schemas (merge all schemas)
|
|
186
540
|
for (const item of schema) {
|
|
187
|
-
|
|
188
|
-
|
|
541
|
+
const definition = unwrapSchemaDefinition(item);
|
|
542
|
+
|
|
543
|
+
if (!definition || !isJSONSchema7(definition)) {
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
Object.assign(zodSchema, jsonSchemaToZod(definition));
|
|
189
548
|
}
|
|
190
|
-
} else if (schema.type ===
|
|
191
|
-
// Handle object schema
|
|
549
|
+
} else if (schema.type === "object" && schema.properties) {
|
|
192
550
|
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
193
551
|
if (!isJSONSchema7(prop)) continue;
|
|
194
552
|
zodSchema[key] = convertPropertyToZod(prop, key, schema);
|
|
@@ -196,4 +554,4 @@ export function jsonSchemaToZod(
|
|
|
196
554
|
}
|
|
197
555
|
|
|
198
556
|
return zodSchema;
|
|
199
|
-
}
|
|
557
|
+
}
|