Skip to content

Instantly share code, notes, and snippets.

@terion-name
Last active July 31, 2025 13:36
Show Gist options
  • Save terion-name/0816a01a1f8c726a05299c824463d40d to your computer and use it in GitHub Desktop.
Save terion-name/0816a01a1f8c726a05299c824463d40d to your computer and use it in GitHub Desktop.
get typed default values from zod schema

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>;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment