Note: I tried to use the accepted answer (.parse({})
), but I often wanted my defaults to be values that wouldn't pass validation (making that method throw errors), and I also was having trouble with nested schema fields.
I was also trying to do this, and came up with the following solution, similar to an earlier answer. Some of the typing isn't the best (usually in the (fieldSchema._def as ...)
areas), but it got rid of my linting errors.
It hasn't been tested thoroughly yet, but the following so far does a good job at creating a "default" object of a given Zod schema, with the following precedence (recursively handling any nested schema fields (z.object(..)
):
- if default value provided for field (
z..default(..)
), immediately return that
- if no default value & is a "base" Zod schema field (e.g.
z.number()
z.string()
, etc.), return a "base" default (I tucked mine away in a BASE_DEFAULTS
object)
- if no default value & is a "transformed" Zod schema field (e.g.
z.refine(..)
, z.coerce(..)
, etc.), then try to take the "inner type" (z.ZodTransformationType<z.ZodInnerTypeIsLocatedHere?, ...>
)
- honestly, never really tested this one -- I just decided that I was going to make it standard practice to always provide a default, then I don't have to deal with transformed types
type ExtractedDefaults<T> = {
[P in keyof T]?: T[P] extends ZodTypeAny ? ReturnType<T[P]["parse"]> : never;
};
export function extractDefaults<TSchema extends ZodRawShape>(
schema: z.ZodObject<TSchema>,
): ExtractedDefaults<TSchema> {
const schemaShape = schema.shape;
const result = {} as ExtractedDefaults<TSchema>;
for (const key in schemaShape) {
const fieldSchema = schemaShape[key];
result[key as keyof TSchema] = extractValueFromSchema(
fieldSchema!,
) as ExtractedDefaults<TSchema>[keyof TSchema];
}
return result;
}
function extractValueFromSchema<T extends ZodTypeAny>(fieldSchema: T): unknown {
if (fieldSchema instanceof z.ZodDefault) {
return (
fieldSchema._def as { defaultValue: () => unknown }
).defaultValue() as ReturnType<T["parse"]>;
} else if (fieldSchema instanceof z.ZodObject) {
return extractDefaultsForm(fieldSchema);
} else if (fieldSchema instanceof z.ZodArray) {
return BASE_DEFAULTS.ARRAY.slice();
} else {
return handleBaseTypes(fieldSchema);
}
}
function handleBaseTypes<T extends ZodTypeAny>(fieldSchema: T): unknown {
switch (fieldSchema.constructor) {
case z.ZodString:
return BASE_DEFAULTS.STRING;
case z.ZodDate:
return BASE_DEFAULTS.STRING;
case z.ZodNumber:
return BASE_DEFAULTS.NUMBER;
case z.ZodBoolean:
return BASE_DEFAULTS.BOOLEAN;
case z.ZodNull:
return BASE_DEFAULTS.NULL;
case z.ZodNullable:
return BASE_DEFAULTS.NULL;
case z.ZodOptional:
return BASE_DEFAULTS.UNDEFINED; // Choose appropriately between UNDEFINED or NULL
default:
return handleTransformedTypes(fieldSchema);
}
}
function handleTransformedTypes<T extends ZodTypeAny>(fieldSchema: T): unknown {
if (
fieldSchema instanceof z.ZodTransformer &&
(fieldSchema._def as { innerType: ZodTypeAny }).innerType
) {
return extractValueFromSchema(
(fieldSchema._def as { innerType: ZodTypeAny }).innerType,
);
}
return BASE_DEFAULTS.UNDEFINED;
}
Note: so far, I'm only using this to create default values to use in react-hook-form. I actually use the BASE_DEFAULTS_FORM below rather than what you'd generally expect some of the defaults to be (since RHF doesn't like undefined, I tend to convert numbers to strings while inside forms to make them easier to work with, etc.).
export const BASE_DEFAULTS = {
STRING: "",
NUMBER: 0,
BOOLEAN: false,
DATE: getTodayPlusTime(), // today date object
OBJECT: {}, // consider if this is best...
ARRAY: [],
NULL: null,
UNDEFINED: undefined,
};
const BASE_DEFAULTS_FORM = {
STRING: "",
NUMBER: null,
BOOLEAN: false,
DATE: getTodayPlusTimeDateString(), // today string (specific format)
OBJECT: {}, // consider if this is best...
ARRAY: [],
NULL: null,
UNDEFINED: null, // consider if this is best...
};
Also, because my typing isn't perfect, I have to typecast where I use it. If anyone can improve the typing, please let me know!
export const schemaCreate = z.object(...)
export const TDocCreate = z.infer(typeof schemaCreate)
export const DEFAULT_VALUES = extractDefaults(
schemaCreate,
) as unknown as TDocCreate;