-
-
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() | |
} |
There is another important point here. Note from the comments at the top of the code:
const defs = defaultInstance<typeof schema>(schema)
The "typeof schema" really should be there. Zod behaves differently when types are not explicitly specified. I have not looked closely at this but I know in my app code there is a difference when the schema is inferred, maybe as z.ZodTypeAny
rather than an explicit type ( from typeof
or from z.infer
). That is, T yields a less specific type when it is not explictly stated, and you might find the final type needs to be recast to typeof schema
to get the exact object type that you desire.
I also note from looking at this code now, that type "T" isn't explicitly used in the function signature:
function defaultInstance<T extends z.ZodTypeAny>(
schema: z.AnyZodObject | z.ZodEffects<any>,
options: object = {}
): z.infer<T> {
I'm considering modifying that to the following:
function defaultInstance<T extends z.AnyZodObject | z.ZodEffects<any>>(
schema: T,
options: object = {}
): z.infer<T> {
This should ensure a result that really is the correct type. Although for run-time, it might be fine if the final object just conforms to the proper shape - and it does that now.
I'll do some testing with this but it would help if anyone else here can also play with this and comment if it makes a difference in your results.
Thanks.
Please post here about your intent, and your (verified please) understanding of what is supposed to happen, and let's see if we can ensure that this works correctly.
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
schema.default({ key: 'some default' })
and another
schema.default({ key: 'other default' })
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:
export function defaultInstance<T extends z.ZodTypeAny>(
schema: z.AnyZodObject | z.ZodDefault<any> | 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.ZodDefault) {
const defValues = schema._def.defaultValue()
const shape = schema._def.innerType._def.shape
const temp = Object.entries(shape).map(([key, value]) => {
if (defValues[key] !== undefined) {
return [key, defValues[key]]
}
else if (value instanceof z.ZodEffects || value instanceof z.ZodDefault) {
return [key, defaultInstance(value as any, options)]
} else {
return [key, getDefaultValue(value as any)]
}
})
return {
...Object.fromEntries(temp),
...defValues
}
}
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.error(`Error: Unable to process this schema`)
return null // unknown or undefined here results in complications
}
function getDefaultValue(dschema: z.ZodTypeAny): any {
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.
@iSplasher I don't think the
.default
method works on a z.object like that. It's intended to work on individual properties, and I don't think it's intended to be applied to the object afterward. But now your original question makes sense to me:You're trying to amend a schema with defaults, outside of the initial z.object creation. Can you confirm the validity of that without using this utility? If that is verified as being valid I guess this utility will need to be modified to accommodate.
For reference, the normal way to do what you're doing there is :
Please post here about your intent, and your (verified please) understanding of what is supposed to happen, and let's see if we can ensure that this works correctly.