-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathzod-compat.ts
More file actions
359 lines (323 loc) · 13.3 KB
/
zod-compat.ts
File metadata and controls
359 lines (323 loc) · 13.3 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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
// zod-compat.ts
// ----------------------------------------------------
// Unified types + helpers to accept Zod v3 and v4 (Mini)
// ----------------------------------------------------
import type * as z3 from 'zod/v3';
import type * as z4 from 'zod/v4/core';
import * as z3rt from 'zod/v3';
import * as z4mini from 'zod/v4-mini';
// --- Unified schema types ---
export type AnySchema = z3.ZodTypeAny | z4.$ZodType;
export type AnyObjectSchema = z3.AnyZodObject | z4.$ZodObject | AnySchema;
export type ZodRawShapeCompat = Record<string, AnySchema>;
// --- Internal property access helpers ---
// These types help us safely access internal properties that differ between v3 and v4
export interface ZodV3Internal {
_def?: {
typeName?: string;
value?: unknown;
values?: unknown[];
shape?: Record<string, AnySchema> | (() => Record<string, AnySchema>);
description?: string;
schema?: AnySchema; // present on ZodEffects (.refine/.superRefine/.transform)
};
shape?: Record<string, AnySchema> | (() => Record<string, AnySchema>);
value?: unknown;
}
export interface ZodV4Internal {
_zod?: {
def?: {
type?: string;
value?: unknown;
values?: unknown[];
shape?: Record<string, AnySchema> | (() => Record<string, AnySchema>);
in?: AnySchema; // present on pipe types (from .transform())
};
};
value?: unknown;
}
// --- Type inference helpers ---
export type SchemaOutput<S> = S extends z3.ZodTypeAny ? z3.infer<S> : S extends z4.$ZodType ? z4.output<S> : never;
export type SchemaInput<S> = S extends z3.ZodTypeAny ? z3.input<S> : S extends z4.$ZodType ? z4.input<S> : never;
/**
* Infers the output type from a ZodRawShapeCompat (raw shape object).
* Maps over each key in the shape and infers the output type from each schema.
*/
export type ShapeOutput<Shape extends ZodRawShapeCompat> = {
[K in keyof Shape]: SchemaOutput<Shape[K]>;
};
// --- Runtime detection ---
export function isZ4Schema(s: AnySchema): s is z4.$ZodType {
// Present on Zod 4 (Classic & Mini) schemas; absent on Zod 3
const schema = s as unknown as ZodV4Internal;
return !!schema._zod;
}
// --- Schema construction ---
export function objectFromShape(shape: ZodRawShapeCompat): AnyObjectSchema {
const values = Object.values(shape);
if (values.length === 0) return z4mini.object({}); // default to v4 Mini
const allV4 = values.every(isZ4Schema);
const allV3 = values.every(s => !isZ4Schema(s));
if (allV4) return z4mini.object(shape as Record<string, z4.$ZodType>);
if (allV3) return z3rt.object(shape as Record<string, z3.ZodTypeAny>);
throw new Error('Mixed Zod versions detected in object shape.');
}
// --- Unified parsing ---
export function safeParse<S extends AnySchema>(
schema: S,
data: unknown
): { success: true; data: SchemaOutput<S> } | { success: false; error: unknown } {
if (isZ4Schema(schema)) {
// Mini exposes top-level safeParse
const result = z4mini.safeParse(schema, data);
return result as { success: true; data: SchemaOutput<S> } | { success: false; error: unknown };
}
const v3Schema = schema as z3.ZodTypeAny;
const result = v3Schema.safeParse(data);
return result as { success: true; data: SchemaOutput<S> } | { success: false; error: unknown };
}
export async function safeParseAsync<S extends AnySchema>(
schema: S,
data: unknown
): Promise<{ success: true; data: SchemaOutput<S> } | { success: false; error: unknown }> {
if (isZ4Schema(schema)) {
// Mini exposes top-level safeParseAsync
const result = await z4mini.safeParseAsync(schema, data);
return result as { success: true; data: SchemaOutput<S> } | { success: false; error: unknown };
}
const v3Schema = schema as z3.ZodTypeAny;
const result = await v3Schema.safeParseAsync(data);
return result as { success: true; data: SchemaOutput<S> } | { success: false; error: unknown };
}
// --- ZodEffects unwrapping ---
/**
* Unwrap ZodEffects wrappers (.superRefine(), .refine(), .transform()) to
* find the inner schema. ZodEffects chains store the wrapped schema in
* `_def.schema`. This walks the chain up to `maxDepth` levels to prevent
* infinite loops on malformed schemas.
*
* Returns the innermost non-ZodEffects schema, or the original schema if
* it is not a ZodEffects.
*/
function unwrapZodEffects(schema: AnySchema, maxDepth = 10): AnySchema {
let current = schema;
for (let i = 0; i < maxDepth; i++) {
const v3 = current as unknown as ZodV3Internal;
if (v3._def?.typeName !== 'ZodEffects' || !v3._def.schema) break;
current = v3._def.schema;
}
return current;
}
// --- Shape extraction ---
export function getObjectShape(schema: AnyObjectSchema | undefined): Record<string, AnySchema> | undefined {
if (!schema) return undefined;
// Zod v3 exposes `.shape`; Zod v4 keeps the shape on `_zod.def.shape`
let rawShape: Record<string, AnySchema> | (() => Record<string, AnySchema>) | undefined;
if (isZ4Schema(schema)) {
const v4Schema = schema as unknown as ZodV4Internal;
rawShape = v4Schema._zod?.def?.shape;
// If no shape found, check if it's a v4 pipe (from .transform())
if (!rawShape && v4Schema._zod?.def?.type === 'pipe' && v4Schema._zod?.def?.in) {
const inner = v4Schema._zod.def.in as unknown as ZodV4Internal;
rawShape = inner._zod?.def?.shape;
}
} else {
const v3Schema = schema as unknown as ZodV3Internal;
rawShape = v3Schema.shape;
// If no shape found, check if this is a ZodEffects wrapping a ZodObject
if (!rawShape) {
const inner = unwrapZodEffects(schema as AnySchema);
if (inner !== schema) {
const innerV3 = inner as unknown as ZodV3Internal;
rawShape = innerV3.shape;
}
}
}
if (!rawShape) return undefined;
if (typeof rawShape === 'function') {
try {
return rawShape();
} catch {
return undefined;
}
}
return rawShape;
}
// --- Schema normalization ---
/**
* Normalizes a schema to an object schema. Handles both:
* - Already-constructed object schemas (v3 or v4)
* - Raw shapes that need to be wrapped into object schemas
*/
export function normalizeObjectSchema(schema: AnySchema | ZodRawShapeCompat | undefined): AnyObjectSchema | undefined {
if (!schema) return undefined;
// First check if it's a raw shape (Record<string, AnySchema>)
// Raw shapes don't have _def or _zod properties and aren't schemas themselves
if (typeof schema === 'object') {
// Check if it's actually a ZodRawShapeCompat (not a schema instance)
// by checking if it lacks schema-like internal properties
const asV3 = schema as unknown as ZodV3Internal;
const asV4 = schema as unknown as ZodV4Internal;
// If it's not a schema instance (no _def or _zod), it might be a raw shape
if (!asV3._def && !asV4._zod) {
// Check if all values are schemas (heuristic to confirm it's a raw shape)
const values = Object.values(schema);
if (
values.length > 0 &&
values.every(
v =>
typeof v === 'object' &&
v !== null &&
((v as unknown as ZodV3Internal)._def !== undefined ||
(v as unknown as ZodV4Internal)._zod !== undefined ||
typeof (v as { parse?: unknown }).parse === 'function')
)
) {
return objectFromShape(schema as ZodRawShapeCompat);
}
}
}
// If we get here, it should be an AnySchema (not a raw shape)
// Check if it's already an object schema
if (isZ4Schema(schema as AnySchema)) {
// Check if it's a v4 object
const v4Schema = schema as unknown as ZodV4Internal;
const def = v4Schema._zod?.def;
if (def && (def.type === 'object' || def.shape !== undefined)) {
return schema as AnyObjectSchema;
}
// Check if it's a v4 pipe type (from .transform()) wrapping an object.
// In Zod v4, .transform() creates a pipe with def.in pointing to the
// input schema. Walk through pipes to find the inner object schema.
if (def?.type === 'pipe' && def.in) {
const inner = def.in as unknown as ZodV4Internal;
const innerDef = inner._zod?.def;
if (innerDef && (innerDef.type === 'object' || innerDef.shape !== undefined)) {
return schema as AnyObjectSchema;
}
}
} else {
// Check if it's a v3 object
const v3Schema = schema as unknown as ZodV3Internal;
if (v3Schema.shape !== undefined) {
return schema as AnyObjectSchema;
}
// Check if it's a v3 ZodEffects wrapping an object schema.
// ZodEffects are created by .superRefine(), .refine(), .transform(), etc.
// They lack .shape but have _def.schema pointing to the inner schema.
// Walk the chain to find the inner ZodObject.
const inner = unwrapZodEffects(schema as AnySchema);
if (inner !== schema) {
const innerV3 = inner as unknown as ZodV3Internal;
if (innerV3.shape !== undefined) {
// Return the original ZodEffects schema — zodToJsonSchema() and
// z4mini.toJSONSchema() can traverse ZodEffects to extract the
// correct JSON Schema from the full chain.
return schema as AnyObjectSchema;
}
}
}
return undefined;
}
function getDotPath(path: (string | number)[]) {
if (path.length === 0) {
return 'object root';
}
return path.reduce((acc, seg, index) => {
if (index === 0) {
return String(seg);
}
if (typeof seg === 'number') {
return `${acc}[${seg}]`;
}
return `${acc}.${seg}`;
}, '');
}
// --- Error message extraction ---
/**
* Safely extracts an error message from a parse result error.
* Zod errors can have different structures, so we handle various cases.
*/
export function getParseErrorMessage(error: unknown): string {
if (error && typeof error === 'object') {
// When present, prioritize zod issues and format as a message and path
if ('issues' in error && Array.isArray(error.issues) && error.issues.length > 0) {
return error.issues
.map((i: { message: string; path?: (string | number)[] }) => {
if (!i.path?.length) {
return i.message;
}
return `${i.message} at ${getDotPath(i.path)}`;
})
.join('\n');
}
// Try common error structures
if ('message' in error && typeof error.message === 'string') {
return error.message;
}
// Fallback: try to stringify the error
try {
return JSON.stringify(error);
} catch {
return String(error);
}
}
return String(error);
}
// --- Schema metadata access ---
/**
* Gets the description from a schema, if available.
* Works with both Zod v3 and v4.
*
* Both versions expose a `.description` getter that returns the description
* from their respective internal storage (v3: _def, v4: globalRegistry).
*/
export function getSchemaDescription(schema: AnySchema): string | undefined {
return (schema as { description?: string }).description;
}
/**
* Checks if a schema is optional.
* Works with both Zod v3 and v4.
*/
export function isSchemaOptional(schema: AnySchema): boolean {
if (isZ4Schema(schema)) {
const v4Schema = schema as unknown as ZodV4Internal;
return v4Schema._zod?.def?.type === 'optional';
}
const v3Schema = schema as unknown as ZodV3Internal;
// v3 has isOptional() method
if (typeof (schema as { isOptional?: () => boolean }).isOptional === 'function') {
return (schema as { isOptional: () => boolean }).isOptional();
}
return v3Schema._def?.typeName === 'ZodOptional';
}
/**
* Gets the literal value from a schema, if it's a literal schema.
* Works with both Zod v3 and v4.
* Returns undefined if the schema is not a literal or the value cannot be determined.
*/
export function getLiteralValue(schema: AnySchema): unknown {
if (isZ4Schema(schema)) {
const v4Schema = schema as unknown as ZodV4Internal;
const def = v4Schema._zod?.def;
if (def) {
// Try various ways to get the literal value
if (def.value !== undefined) return def.value;
if (Array.isArray(def.values) && def.values.length > 0) {
return def.values[0];
}
}
}
const v3Schema = schema as unknown as ZodV3Internal;
const def = v3Schema._def;
if (def) {
if (def.value !== undefined) return def.value;
if (Array.isArray(def.values) && def.values.length > 0) {
return def.values[0];
}
}
// Fallback: check for direct value property (some Zod versions)
const directValue = (schema as { value?: unknown }).value;
if (directValue !== undefined) return directValue;
return undefined;
}