Retrieve default values from schema #1953
-
I am trying to build a form abstraction around Zod. Currently it works as follows: const = useForm({
schema: z.object({
name: z.string(),
demo: z.number(),
}),
// This object is of type `Partial<z.infer<S>>` where `S` is the schema above.
defaultValues: {
name: "todo"
}
}); Now I would like to remove the const = useForm({
schema: z.object({
name: z.string().default("todo"),
demo: z.number(),
}),
}); The way I saw in issue #201 it was recommended to use In issue #213 there was another workaround but it would greatly comprise DX so it doesn't work for me either. I think this could be fixed by either:
Thanks for any help that can be provided and I have been loving using Zod in my projects! |
Beta Was this translation helpful? Give feedback.
Replies: 15 comments 18 replies
-
Is this what you are looking for? function getDefaults<Schema extends z.AnyZodObject>(schema: Schema) {
return Object.fromEntries(
Object.entries(schema.shape).map(([key, value]) => {
if (value instanceof z.ZodDefault) return [key, value._def.defaultValue()]
return [key, undefined]
})
)
}
const schema = z.object({
name: z.string().default('todo'),
demo: z.number(),
})
console.log(getDefaults(schema))
// { name: 'todo', demo: undefined } |
Beta Was this translation helpful? Give feedback.
-
This feature will be quite useful, for providing initial value to the form, |
Beta Was this translation helpful? Give feedback.
-
@JacobWeisenburger that worked perfectly! Thanks for your help! I do agree with @naviens it would be nice to have this as a helper in Zod itself but if that's not something that would be considered this issue can be closed. |
Beta Was this translation helpful? Give feedback.
-
I would also prefer a native support in zod to access the structure - my current "hack" uses Zod to Json Schema to export the schema to a JSON structure and then parse this to an object which only contains the props with the default values. This code is not been tested in production and might break in many cases! import { zodToJsonSchema } from "zod-to-json-schema";
import { ZodSchema } from 'zod';
type PropertySchema = {
default?: any;
type: 'object' | 'array' | 'string' | 'number';
properties?: Record<string, PropertySchema>;
items?: PropertySchema;
};
function getProperties(defaults: Record<string, any>, properties?: Record<string, PropertySchema>) {
if (!properties) {
return;
}
Object.keys(properties).forEach((prop) => {
if (properties[prop].hasOwnProperty('default')) {
defaults[prop] = properties[prop].default;
} else if (properties[prop].type === 'object') {
defaults[prop] = {};
getProperties(defaults[prop], properties[prop].properties);
} else if (properties[prop].type === 'array') {
defaults[prop] = [];
if (properties[prop].items && properties?.[prop]?.items?.type === 'object') {
defaults[prop].push({});
getProperties(defaults[prop][0], properties?.[prop]?.items?.properties);
}
} else if (properties[prop].type === 'string') {
defaults[prop] = '';
} else if (properties[prop].type === 'number') {
defaults[prop] = 0;
}
});
}
export function jsonSchemaExtractDefaults(schema: { definitions: Record<string, { properties: Record<string, PropertySchema> }>, $ref: string }) {
const { properties } = schema.definitions[schema.$ref.split('/').pop() as string];
const result: Record<string, any> = {};
getProperties(result, properties);
return result;
}
export function zodExtractDefaults<T extends ZodSchema<any>>(schema: T) {
const jsonSchema = zodToJsonSchema(schema, 'tempSchema');
// @ts-ignore
return jsonSchemaExtractDefaults(jsonSchema);
} zod schema const idType = "Place"
const idRegex = new RegExp(idType + `#[A-Za-z0-9_-]{21}`)
const PlaceId = z.string().regex(idRegex).default('')
const TemplateProps = z
.object({
placeId: PlaceId,
paxMax: z.number().min(0).default(30),
groupMax: z.number().min(0).default(2),
groupPaxMax: z.number().min(0).default(30)
})
.refine((schema) => schema.groupMax <= schema.groupPaxMax, {
message: "groupMax>groupMax to big!",
path: ["groupMax"]
})
.refine((schema) => schema.groupPaxMax <= schema.paxMax, {
message: "groupPaxMax>PaxMax",
path: ["groupPaxMax"]
});
const mschema = z.object({
name: z.string().min(1).max(50).default(""),
props: TemplateProps
}); JSON Schema with Zod to Json Schema {
"$ref": "#/definitions/mySchema",
"definitions": {
"mySchema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"maxLength": 50,
"default": ""
},
"props": {
"type": "object",
"properties": {
"placeId": {
"type": "string",
"pattern": "Place#[A-Za-z0-9_-]{21}",
"default": ""
},
"paxMax": {
"type": "number",
"minimum": 0,
"default": 30
},
"groupMax": {
"type": "number",
"minimum": 0,
"default": 2
},
"groupPaxMax": {
"type": "number",
"minimum": 0,
"default": 30
}
},
"required": [
"placeId"
],
"additionalProperties": false
}
},
"required": [
"name",
"props"
],
"additionalProperties": false
}
},
"$schema": "http://json-schema.org/draft-07/schema#"
} defaults only {
"name": "",
"props": {
"placeId": "",
"paxMax": 30,
"groupMax": 2,
"groupPaxMax": 30
}
} |
Beta Was this translation helpful? Give feedback.
-
This was my crack at it. It works recursively, grabbing the default of any zod schema type if there is one, and export function getDefaults<TSchema extends z.AnyZodObject>(schema: TSchema) {
function getDefaultValue(schema: z.ZodTypeAny): unknown {
if (schema instanceof z.ZodDefault) return schema._def.defaultValue();
if (!("innerType" in schema._def)) return undefined;
return getDefaultValue(schema._def.innerType);
}
return Object.fromEntries(
Object.entries(schema.shape).map(([key, value]) => {
return [key, getDefaultValue(value)];
})
);
} It's not perfect though. I get the typescript warning: Result const defaults = getDefaults(
z.object({
name: z.string().default("todo"),
demo: z.number(),
})
);
console.log(defaults); {
name: "todo",
demo: undefined
} And a more complex example const defaults = getDefaults(
z.object({
cache: z.boolean().default(true),
cache_expiration: z.number().default(60),
run_again_delay: z.number().min(1).default(1),
email: z.string().email().default("asdf@example.com"),
playlist_report: z.boolean().optional().default(false),
custom_repo: z.string().optional().default(""),
verify_ssl: z.boolean().optional().default(true),
check_nightly: z.boolean().nullable().default(false),
})
);
console.log(defaults); {
cache: true,
cache_expiration: 60,
run_again_delay: 1,
email: "asdf@example.com",
playlist_report: false,
custom_repo: "",
verify_ssl: true,
check_nightly: false,
} I'll also echo what others have stated, saying that Zod should provide this functionality natively. |
Beta Was this translation helpful? Give feedback.
-
Hi, this works for me for with refine, pipe and recursivly obj: const validatorSchema = z.object({
firstname: z.string().min(2, "error on firstname"),
lastname: z.string().transform((val) => val.length).pipe(z.number().min(5)),
age: z.number(),
address: z.object({
address1: z.string().nonempty(),
address2: z.string().nonempty(),
country: z.string().nonempty(),
insideobj: z.object({
id: z.string(),
insideobj: z.object({
id: z.string()
}),
}),
code: z.array(z.object({
id: z.string(),
somevalue: z.number().nonpositive(),
insideobj: z.object({
id: z.string()
})
}))
})
})
.refine(({firstname, age}) => firstname ? age > 18 : true, {"message": "Age must be sup to 18"} )
.refine(schema => true)
function getDefaults<T extends z.ZodTypeAny>( schema: z.AnyZodObject | z.ZodEffects<any> ): z.infer<T> {
// Check if it's a ZodEffect
if (schema instanceof z.ZodEffects) {
// Check if it's a recursive ZodEffect
if (schema.innerType() instanceof z.ZodEffects) return getDefaults(schema.innerType())
// return schema inner shape as a fresh zodObject
return getDefaults(z.ZodObject.create(schema.innerType().shape))
}
function getDefaultValue(schema: z.ZodTypeAny): unknown {
if (schema instanceof z.ZodDefault) return schema._def.defaultValue();
// return an empty array if it is
if (schema instanceof z.ZodArray) return [];
// return an empty string if it is
if (schema instanceof z.ZodString) return "";
// return an content of object recursivly
if (schema instanceof z.ZodObject) return getDefaults(schema);
if (!("innerType" in schema._def)) return undefined;
return getDefaultValue(schema._def.innerType);
}
return Object.fromEntries(
Object.entries( schema.shape ).map( ( [ key, value ] ) => {
return [key, getDefaultValue(value)];
} )
)
}
const defaultValue = getDefaults<typeof validatorSchema>(validatorSchema) output: {
"address1": "",
"address2": "",
"country": "",
"insideobj": {
"id": "",
"insideobj": {
"id": ""
}
},
"code": []
} |
Beta Was this translation helpful? Give feedback.
-
Building on the recursive model provided by others here, I'm providing an update that accommodates all issues and schema to-date. This is all just a work-in-progress. There are still anomalies that should be resolved - see below. But so far I'm able to use this for my purposes and I think it's pretty good. Features / Solved Issues
Anomalies
Bottom line : It's REALLY REALLY REALLY important, if you want a default object from your schema, to set a .default() for all primitives and constructs. This code will process that. If you don't provide a default in the schema, it might be a long time before a feature like this can be built into Zod as a reliable component. Function defaultInstanceimport { z } from 'zod'
/**
* defaultInstance
* @param schema z.object schema definition
* @param options Optional object, may include property:
* - defaultArrayEmpty: true
* @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,{ defaultArrayEmpty: true} )
*/
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
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 {
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) {
let value = dschema.minValue ?? 0
return value
}
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()
} EDIT: Neglected to include TestSchema230512export const TestSchema230512 = z
.object({
firstname: z.string().min(2, 'error on firstname'),
lastname_with_pipe: z
.string()
.default('smith')
.transform(val => val.length)
.pipe(z.number().min(5)),
age: z.number(),
location_with_refine: z
.object({
address: z.object({
street1: z.string(),
street2: z.string(),
}),
country: z.object({
id: z.object({
alpha2: z.string().length(2),
alpha3: z.string().length(3),
}),
name: z.string().nonempty().default('Not Specified'),
}),
})
.refine(
the_location => the_location.address.street2 !== '' && the_location.address.street1 === '',
'Please move street2 to street1'
),
pets_no_defaults: z.array(
z.object({
name: z.string(),
age: z.number().nonnegative().lte(250) // handle turtles,
})
),
children_with_defaults: z
.array(
z
.object({
name: z.string(),
age: z.number().nonnegative().lte(120),
})
.default({ name: 'no name', age: 0 })
)
.default([{ name: 'no children', age: -1 }]),
})
.refine(
me => me.age < 12,
"You aren't really less than 12 years of age are ya?"
) EDIT: Default object from TestSchema230512let default1 = defaultInstance<typeof TestSchema230512>(TestSchema230512
, {defaultArrayEmpty: true} ) {
"schema": "TestSchema230512",
"default": {
"firstname": "",
"lastname_with_pipe": 5,
"age": 0,
"location_with_refine": {
"address": {
"street1": "",
"street2": ""
},
"country": {
"id": {
"alpha2": "",
"alpha3": ""
},
"name": "Not Specified"
}
},
"pets_no_defaults": [], << No default element with option defaultArrayEmpty: true
"children_with_defaults": [
{
"name": "no children",
"age": -1
}
]
}
} EDIT: 23/05/15 : Added support for ZodDate with new options Further updates will be posted in this gist: |
Beta Was this translation helpful? Give feedback.
-
Can't believe you have you have to write own boilerplate method for such basic operation. |
Beta Was this translation helpful? Give feedback.
-
Noting this in case others have my use case: If all of your schema keys have default values, then you can just do export const SCHEMA_DEFAULTS = schema.parse({}); |
Beta Was this translation helpful? Give feedback.
-
@AleksandrHovhannisyan but it requires us to put valid default values, we cannot prefill the whole form with valid default values. |
Beta Was this translation helpful? Give feedback.
-
I had started to write an npm package that tries to be an answer to this problem in its own way. I stumbled upon this thread after creating the npm package, so I'm sorry if some things seem repetitive. While I understand the different points that have been mentioned in this thread, it seems there are still some valid use cases (ex: generating forms with zod) where it is desirable to have "default" or "empty" values matching a Zod schema. My personal use case did not require anything too strict: I needed an object with values that are not necessarily "valid" according to the schema, but are "correct" in the sense of an empty form. If the schema is For this specific use-case, I started an npm package where I welcome PRs: If this was the wrong place to post this I apologize, please guide me as to where this belongs. |
Beta Was this translation helpful? Give feedback.
-
Any news on this? |
Beta Was this translation helpful? Give feedback.
-
+1 would love to have this supported by default by zod |
Beta Was this translation helpful? Give feedback.
-
Migrating from yup which has |
Beta Was this translation helpful? Give feedback.
-
my 2c with result typing.
|
Beta Was this translation helpful? Give feedback.
Is this what you are looking for?