for zod3.ts works and types correctly
zod4 version yet requires some adjustments and tests
import { z } from 'zod'; | |
// biome-ignore lint/suspicious/noExplicitAny: generic matchers are ok | |
type UnwrapEffects<T> = T extends z.ZodEffects<infer I, any, any> | |
? UnwrapEffects<I> | |
: T; | |
type ShapeOf<T> = UnwrapEffects<T> extends z.ZodObject<infer S> ? S : never; | |
type Defaults<T extends z.ZodRawShape> = { | |
// keep the key when, **after** unwrapping effects, it is: | |
// • a ZodDefault → we take its output type | |
// • a ZodObject → we recurse into its shape | |
// biome-ignore lint/suspicious/noExplicitAny: generic matchers are ok | |
[K in keyof T as UnwrapEffects<T[K]> extends z.ZodDefault<any> | |
? K | |
: ShapeOf<T[K]> extends never | |
? never | |
: // biome-ignore lint/suspicious/noExplicitAny: generic matchers are ok | |
K]: UnwrapEffects<T[K]> extends z.ZodDefault<any> | |
? z.infer<T[K]> // keep the effects’ output type | |
: Defaults<ShapeOf<T[K]>>; // recurse | |
}; | |
function unwrapEffects<T extends z.ZodTypeAny>(schema: T | z.ZodEffects<T>): T { | |
return schema instanceof z.ZodEffects ? schema.innerType() : schema; | |
} | |
export function getZodDefaults<T extends z.ZodRawShape>( | |
schema: z.ZodObject<T> | z.ZodEffects<z.ZodObject<T>>, | |
): Defaults<T> { | |
const zObject = schema instanceof z.ZodEffects ? schema.innerType() : schema; | |
return Object.fromEntries( | |
Object.entries(zObject.shape) | |
.map(([k, vv]) => { | |
const v = unwrapEffects(vv); | |
return [ | |
k, | |
v instanceof z.ZodDefault | |
? v._def.defaultValue() | |
: v instanceof z.ZodObject | |
? getZodDefaults(v) | |
: undefined, | |
]; | |
}) | |
.filter(([_k, v]) => v !== undefined), | |
) as Defaults<T>; | |
} |
/** biome-ignore lint/suspicious/noExplicitAny: match in generics it's ok */ | |
import z from 'zod'; | |
// Strip wrappers until we get the naked ZodObject instance | |
const unwrapObject = (s: z.ZodTypeAny): z.ZodObject => { | |
if (s instanceof z.ZodObject) { | |
return s; | |
} | |
if ('innerType' in s && typeof s.innerType === 'function') { | |
// ZodPipe keeps the original schema in .innerType() | |
return unwrapObject(s.innerType()); | |
} | |
if ('_zod' in s && 'def' in s._zod && 'inner' in s._zod.def) { | |
// ZodTransform keeps it in _zod.def.inner | |
return unwrapObject(s._zod.def.inner as z.ZodTypeAny); | |
} | |
throw new Error('Expected an object schema or a wrapper around one'); | |
}; | |
// recursive Defaults<> – now aware of ZodPipe / ZodTransform | |
export type Defaults<T extends z.ZodRawShape> = { | |
[K in keyof T as T[K] extends z.ZodDefault<any> | |
? K | |
: T[K] extends z.ZodObject<any> | |
? K | |
: T[K] extends z.ZodPipe<z.ZodObject<any>, any> | |
? K | |
: T[K] extends z.ZodTransform<z.ZodObject<any>, any> | |
? K | |
: never]: T[K] extends z.ZodDefault<infer _U> | |
? z.output<T[K]> | |
: T[K] extends z.ZodObject<infer SH> | |
? Defaults<SH> | |
: T[K] extends z.ZodPipe<z.ZodObject<infer SH>, any> | |
? Defaults<SH> | |
: T[K] extends z.ZodTransform<z.ZodObject<infer SH>, any> | |
? Defaults<SH> | |
: never; | |
}; | |
export function getZodDefaults< | |
T extends z.ZodRawShape, | |
S extends | |
| z.ZodObject<T> | |
| z.ZodPipe<z.ZodObject<T>, any> | |
| z.ZodTransform<z.ZodObject<T>, any>, | |
>(schema: S): Defaults<T> { | |
const obj = unwrapObject(schema) as z.ZodObject<T>; | |
return Object.fromEntries( | |
Object.entries(obj.shape) | |
.map(([key, field]) => { | |
// plain default | |
if (field instanceof z.ZodDefault) { | |
const def = field.def || field._zod?.def || field._def; // https://github.com/colinhacks/zod/issues/5020 | |
return [ | |
key, | |
typeof def.defaultValue === 'function' | |
? def.defaultValue() | |
: def.defaultValue, | |
]; | |
} | |
// nested object (possibly piped / transformed) | |
if ( | |
field instanceof z.ZodObject || | |
('innerType' in field && typeof field.innerType === 'function') || // ZodPipe is not a class? | |
('_zod' in field && 'def' in field._zod && 'inner' in field._zod.def) // ZodTransform is not a class? | |
) { | |
return [ | |
key, | |
getZodDefaults( | |
field as | |
| z.ZodObject | |
| z.ZodPipe<z.ZodObject<any>, any> | |
| z.ZodTransform<z.ZodObject<any>, any>, | |
), | |
]; | |
} | |
// no default for this key | |
return [key, undefined]; | |
}) | |
// drop keys that didn’t produce anything | |
.filter(([, v]) => v !== undefined), | |
) as Defaults<T>; | |
} |