-
-
Save TonyGravagno/2b744ceb99e415c4b53e8b35b309c29c to your computer and use it in GitHub Desktop.
| import { z } from 'zod' | |
| /** | |
| * @summary Function returns default object from Zod schema | |
| * @version 23.05.15.2 | |
| * @link https://gist.github.com/TonyGravagno/2b744ceb99e415c4b53e8b35b309c29c | |
| * @author Jacob Weisenburger, Josh Andromidas, Thomas Moiluiavon, Tony Gravagno | |
| * @param schema z.object schema definition | |
| * @param options Optional object, see Example for details | |
| * @returns Object of type schema with defaults for all fields | |
| * @example | |
| * const schema = z.object( { ... } ) | |
| * const default1 = defaultInstance<typeof schema>(schema) | |
| * const default2 = defaultInstance<typeof schema>( | |
| * schema,{ // toggle from these defaults if required | |
| * defaultArrayEmpty: false, | |
| * defaultDateEmpty: false, | |
| * defaultDateUndefined: false, | |
| * defaultDateNull: false, | |
| * } ) | |
| */ | |
| export function defaultInstance<T extends z.ZodTypeAny>( | |
| schema: z.AnyZodObject | z.ZodEffects<any>, | |
| options: object = {} | |
| ): z.infer<T> { | |
| const defaultArrayEmpty = 'defaultArrayEmpty' in options ? options.defaultArrayEmpty : false | |
| const defaultDateEmpty = 'defaultDateEmpty' in options ? options.defaultDateEmpty : false | |
| const defaultDateUndefined = 'defaultDateUndefined' in options ? options.defaultDateUndefined : false | |
| const defaultDateNull = 'defaultDateNull' in options ? options.defaultDateNull : false | |
| function run(): z.infer<T> { | |
| if (schema instanceof z.ZodEffects) { | |
| if (schema.innerType() instanceof z.ZodEffects) { | |
| return defaultInstance(schema.innerType(), options) // recursive ZodEffect | |
| } | |
| // return schema inner shape as a fresh zodObject | |
| return defaultInstance(z.ZodObject.create(schema.innerType().shape), options) | |
| } | |
| if (schema instanceof z.ZodType) { | |
| let the_shape = schema.shape as z.ZodAny // eliminates 'undefined' issue | |
| let entries = Object.entries(the_shape) | |
| let temp = entries.map(([key, value]) => { | |
| let this_default = | |
| value instanceof z.ZodEffects ? defaultInstance(value, options) : getDefaultValue(value) | |
| return [key, this_default] | |
| }) | |
| return Object.fromEntries(temp) | |
| } else { | |
| console.log(`Error: Unable to process this schema`) | |
| return null // unknown or undefined here results in complications | |
| } | |
| function getDefaultValue(dschema: z.ZodTypeAny): any { | |
| console.dir(dschema) | |
| if (dschema instanceof z.ZodDefault) { | |
| if (!('_def' in dschema)) return undefined // error | |
| if (!('defaultValue' in dschema._def)) return undefined // error | |
| return dschema._def.defaultValue() | |
| } | |
| if (dschema instanceof z.ZodArray) { | |
| if (!('_def' in dschema)) return undefined // error | |
| if (!('type' in dschema._def)) return undefined // error | |
| // return empty array or array with one empty typed element | |
| return defaultArrayEmpty ? [] : [getDefaultValue(dschema._def.type as z.ZodAny)] | |
| } | |
| if (dschema instanceof z.ZodString) return '' | |
| if (dschema instanceof z.ZodNumber || dschema instanceof z.ZodBigInt) { | |
| let value = dschema.minValue ?? 0 | |
| return value | |
| } | |
| if (dschema instanceof z.ZodDate) { | |
| let value = defaultDateEmpty | |
| ? '' | |
| : defaultDateNull | |
| ? null | |
| : defaultDateUndefined | |
| ? undefined | |
| : (dschema as z.ZodDate).minDate | |
| return value | |
| } | |
| if (dschema instanceof z.ZodSymbol) return '' | |
| if (dschema instanceof z.ZodBoolean) return false | |
| if (dschema instanceof z.ZodNull) return null | |
| if (dschema instanceof z.ZodPipeline) { | |
| if (!('out' in dschema._def)) return undefined // error | |
| return getDefaultValue(dschema._def.out) | |
| } | |
| if (dschema instanceof z.ZodObject) { | |
| return defaultInstance(dschema, options) | |
| } | |
| if (dschema instanceof z.ZodAny && !('innerType' in dschema._def)) return undefined // error? | |
| return getDefaultValue(dschema._def.innerType) | |
| } | |
| } | |
| return run() | |
| } |
Thanks for the update! I'll check through it as soon as I can, and if it doesn't seem to break anything I'll replace the OP.
Has this been resolved?
Would it make sense/possible to abstract it to also work for .catch()?
Thanks for your contribution, I really appreciate it.
Update of ... non updates. I'm sorry that I haven't had time to come back to enhance this code. I was hoping that by now there would be a solution implemented in Zod, and have been holding out on continuing updates here. I'll hold out a bit longer. I haven't been active in my apps that use Zod recently so I'm a bit rusty on all of this.
The code here isn't ideal. There are a number of faults in more complex use cases. Here is my suggested approach to addressing this topic:
- Watch this gist, but don't count on it being updated or for generous submissions to be incorporated.
- Watch colinhacks/zod#1953
- Be aware that any system that provides a default value for general use cases may not be ideal for a large number of specific use cases.
- Get lots of sleep. Drink lots of coffee. Be good to yourself and others.
- Have a great day.
Anyone have a chance to see if the new zod version improves this experience?
Update: Upgrading to
zod@4beta breaks this function and the new version doesn't appear to solve the default schema problem
For the veterans of this gist: zod v4 compatible edition
import { z } from 'zod';
/**
* @summary Function returns default object from Zod schema (Zod v4 compatible)
* @version 24.0.0
* @link https://gist.github.com/TonyGravagno/2b744ceb99e415c4b53e8b35b309c29c
* @author Jacob Weisenburger, Josh Andromidas, Thomas Moiluiavon, Tony Gravagno, Vladislav Markov
* @param schema Zod schema definition
* @param options Optional configuration object
* @param options.defaultArrayEmpty If true, arrays return empty array; if false, array with one default element
* @param options.defaultDateEmpty If true, dates return empty string
* @param options.defaultDateUndefined If true, dates return undefined
* @param options.defaultDateNull If true, dates return null
* @returns Object of type schema with defaults for all fields
* @example
* const schema = z.object( { ... } )
* const default1 = defaultInstance<typeof schema>(schema)
* const default2 = defaultInstance<typeof schema>(
* schema,{ // toggle from these defaults if required
* defaultArrayEmpty: false,
* defaultDateEmpty: false,
* defaultDateUndefined: false,
* defaultDateNull: false,
* } )
*/
export interface DefaultInstanceOptions {
defaultArrayEmpty?: boolean;
defaultDateEmpty?: boolean;
defaultDateUndefined?: boolean;
defaultDateNull?: boolean;
}
export function defaultInstance<T extends z.ZodType>(schema: z.ZodType, options: DefaultInstanceOptions = {}): z.infer<T> {
const {
defaultArrayEmpty = false,
defaultDateEmpty = false,
defaultDateUndefined = false,
defaultDateNull = false,
} = options;
function isZodObject(s: z.ZodType): s is z.ZodObject<any> {
return s instanceof z.ZodObject || (s && typeof s === 'object' && 'shape' in s && typeof (s as any).shape === 'object');
}
function run(): z.infer<T> {
if (schema instanceof z.ZodTransform) {
const def = (schema as any)._def;
if (def && 'schema' in def) {
const innerSchema = def.schema as z.ZodType;
return defaultInstance(innerSchema, options);
}
}
if (isZodObject(schema)) {
const shape = schema.shape;
if (!shape) {
return {} as z.infer<T>;
}
return Object.fromEntries(
Object.entries(shape).map(([key, value]) => {
const val = value as z.ZodType;
const this_default = val instanceof z.ZodTransform ? defaultInstance(val, options) : getDefaultValue(val);
return [key, this_default];
}),
) as z.infer<T>;
}
return getDefaultValue(schema) as z.infer<T>;
function getDefaultValue(dschema: z.ZodType): any {
function extractDefaultValue(s: z.ZodDefault | z.ZodPrefault): any {
const def = (s as any)._def;
if (def && 'defaultValue' in def) {
const defaultValue = def.defaultValue as unknown;
return typeof defaultValue === 'function' ? (defaultValue as () => any)() : defaultValue;
}
return getDefaultValue(s.unwrap() as z.ZodType);
}
function getArrayElementType(arraySchema: z.ZodArray<any>): z.ZodType | null {
if ((arraySchema as any).element) {
return (arraySchema as any).element as z.ZodType;
}
const def = (arraySchema as any)._def;
if (def && 'element' in def) {
return def.element as z.ZodType;
}
return null;
}
if (isZodObject(dschema)) {
return defaultInstance(dschema, options);
}
if (dschema instanceof z.ZodDefault) {
return extractDefaultValue(dschema);
}
if (dschema instanceof z.ZodPrefault) {
return extractDefaultValue(dschema);
}
if (dschema instanceof z.ZodOptional) {
return getDefaultValue(dschema.unwrap() as z.ZodType);
}
if (dschema instanceof z.ZodNullable) {
return getDefaultValue(dschema.unwrap() as z.ZodType);
}
if (dschema instanceof z.ZodArray) {
const elementType = getArrayElementType(dschema);
if (elementType) {
return defaultArrayEmpty ? [] : [getDefaultValue(elementType)];
}
return [];
}
if (dschema instanceof z.ZodString) return '';
if (dschema instanceof z.ZodNumber || dschema instanceof z.ZodBigInt) {
return (dschema as any).minValue ?? 0;
}
if (dschema instanceof z.ZodDate) {
return defaultDateEmpty
? ''
: defaultDateNull
? null
: defaultDateUndefined
? undefined
: (dschema as any).minDate;
}
if (dschema instanceof z.ZodSymbol) return '';
if (dschema instanceof z.ZodBoolean) return false;
if (dschema instanceof z.ZodNull) return null;
if (dschema && typeof dschema === 'object' && '_def' in dschema && 'out' in (dschema as any)._def) {
const pipeOut = (dschema as any)._def.out as z.ZodType;
if (pipeOut) {
return getDefaultValue(pipeOut);
}
}
if (dschema instanceof z.ZodRecord) {
return {};
}
if (dschema instanceof z.ZodAny || dschema instanceof z.ZodUnknown || dschema instanceof z.ZodCustom) {
return undefined;
}
if (dschema && typeof dschema === 'object' && '_def' in dschema) {
const def = (dschema as any)._def;
if ('innerType' in def && def.innerType) {
return getDefaultValue(def.innerType as z.ZodType);
}
if ('unwrap' in dschema && typeof dschema.unwrap === 'function') {
try {
return getDefaultValue(dschema.unwrap() as z.ZodType);
} catch {
// ignore
}
}
}
return undefined;
}
}
return run();
}total: z.number() defaults to total: -Infinity
fix:
if (dschema instanceof z.ZodNumber || dschema instanceof z.ZodBigInt) {
const minValue = (dschema as any).minValue
return minValue && minValue !== -Infinity ? minValue : null
}
We're using zod with react-hook-form and want to provide defaults to the forms while keeping our schema reusable.
In one part of our code base we do
and another
It's true that the resulting schema won't make use of the defaults, however, like this, we get automatic type support while keeping the defaults close to where they're needed :)
I took a shot at the implementation: