I have a zod object that is defined like this:
const schema = z.object({
firstName: z.string().min(1, 'Required'),
middleName: z.string().optional(),
lastName: z.string().min(1, 'Required'),
phoneCountryCode: z.string().min(1, 'Required'),
phoneNumber: z.string().min(1, 'Required'),
countryOfResidence: z.string().min(1, 'Required'),
});
How can I extract the keys from the ZodObject
?
Obviously, I could define the input object
first and then insert it as an argument for z.object
const schemaRawShape = {
firstName: z.string().min(1, 'Required'),
middleName: z.string().optional(),
lastName: z.string().min(1, 'Required'),
phoneCountryCode: z.string().min(1, 'Required'),
phoneNumber: z.string().min(1, 'Required'),
countryOfResidence: z.string().min(1, 'Required'),
} satisfies z.ZodRawShape
const schema = z.object(schemaRawShape);
const keys = Object.keys(infoStepSchemaCommonObj)
However, that bees more difficult to read and follow as the schema grows more plicated.
For example, when dealing with unions and/or intersections.
See discussion on github
EDIT:
I should have made this clear earlier, but it we must be able to extract keys from unions, intersections and other more plex schemas.
This schema
will not have shape
and thus the snippet will throw an error:
<script type="module">
import { z } from "/[email protected]";
const schema1 = z.object({
firstName: z.string().min(1, "Required"),
middleName: z.string().optional(),
lastName: z.string().min(1, "Required"),
phoneCountryCode: z.string().min(1, "Required"),
phoneNumber: z.string().min(1, "Required"),
countryOfResidence: z.string().min(1, "Required"),
});
const schema2 = z.object({
more: z.string()
})
const schema = schema1.and(schema2)
const propertyNames = Object.keys(schema.shape);
console.log(propertyNames);
</script>
I have a zod object that is defined like this:
const schema = z.object({
firstName: z.string().min(1, 'Required'),
middleName: z.string().optional(),
lastName: z.string().min(1, 'Required'),
phoneCountryCode: z.string().min(1, 'Required'),
phoneNumber: z.string().min(1, 'Required'),
countryOfResidence: z.string().min(1, 'Required'),
});
How can I extract the keys from the ZodObject
?
Obviously, I could define the input object
first and then insert it as an argument for z.object
const schemaRawShape = {
firstName: z.string().min(1, 'Required'),
middleName: z.string().optional(),
lastName: z.string().min(1, 'Required'),
phoneCountryCode: z.string().min(1, 'Required'),
phoneNumber: z.string().min(1, 'Required'),
countryOfResidence: z.string().min(1, 'Required'),
} satisfies z.ZodRawShape
const schema = z.object(schemaRawShape);
const keys = Object.keys(infoStepSchemaCommonObj)
However, that bees more difficult to read and follow as the schema grows more plicated.
For example, when dealing with unions and/or intersections.
See discussion on github
EDIT:
I should have made this clear earlier, but it we must be able to extract keys from unions, intersections and other more plex schemas.
This schema
will not have shape
and thus the snippet will throw an error:
<script type="module">
import { z } from "https://esm.sh/[email protected]";
const schema1 = z.object({
firstName: z.string().min(1, "Required"),
middleName: z.string().optional(),
lastName: z.string().min(1, "Required"),
phoneCountryCode: z.string().min(1, "Required"),
phoneNumber: z.string().min(1, "Required"),
countryOfResidence: z.string().min(1, "Required"),
});
const schema2 = z.object({
more: z.string()
})
const schema = schema1.and(schema2)
const propertyNames = Object.keys(schema.shape);
console.log(propertyNames);
</script>
Edit - Full example with intersections and unions
import { differenceInYears } from 'date-fns';
import * as z from 'zod';
const password = z
.string()
.min(10, 'Must be at least 10 characters long')
.regex(/\d/g, 'Must contain a number')
.regex(/[a-z]/g, 'Must contain a lower case character')
.regex(/[A-Z]/g, 'Must contain an upper case character')
.regex(/[^\w\d]/g, 'Must contain a special character');
const infoStepSchemaCommon = z.object({
firstName: z.string().min(1, 'Required'),
middleName: z.string().optional(),
lastName: z.string().min(1, 'Required'),
phoneCountryCode: z.string().min(1, 'Required'),
phoneNumber: z.string().min(1, 'Required'),
countryOfResidence: z.string().min(1, 'Required'),
});
const coerceNumber = z.coerce.number({
required_error: 'Required',
invalid_type_error: 'Must be a number',
});
const ageRestrictionString = 'Must be at least 12 years old';
const infoStepSchemaWithoutNationalId = z
.object({
hasNationalId: z.literal(false).optional(),
birthMonth: coerceNumber.min(1).max(12),
birthDay: coerceNumber.min(1).max(31, 'No month has more than 31 days'),
birthYear: coerceNumber.min(1900).max(new Date().getFullYear()),
})
.refine(
(d) =>
differenceInYears(
new Date(),
new Date(d.birthYear, d.birthMonth - 1, d.birthDay)
) >= 12,
{ message: ageRestrictionString, path: ['birthYear'] }
)
.and(infoStepSchemaCommon);
const infoStepSchemaWithNationalId = z
.object({
hasNationalId: z.literal(true),
nationalId: z
.string()
.min(1, 'Required')
.min(10, 'Must contain at least 10 digits')
.max(10, 'Must not contain more than 10 digits')
.regex(/^\d+$/, 'Must only contain numbers')
})
.and(infoStepSchemaCommon);
export const emailStepSchema = z.object({
email: z.string().min(1, 'Required').email(),
});
export const infoStepSchema = infoStepSchemaWithoutNationalId.or(
infoStepSchemaWithNationalId
);
export const passwordStepSchema = z
.object({
languageId: z.string(),
password,
passwordAgain: password,
privacyPolicyAccepted: z.coerce
.boolean()
.refine((v) => v, 'Must accept to continue'),
clubConditionsAccepted: z.coerce
.boolean()
.refine((v) => v, 'Must accept to continue'),
})
.refine((data) => data.password === data.passwordAgain, {
message: "Passwords don't match",
path: ['passwordAgain'],
});
export const signupFormSchema = emailStepSchema
.and(infoStepSchema)
.and(passwordStepSchema);
- Goal 1: Extract all possible keys from
signupFormSchema
- Goal 2: Extract all possible keys from each "step" schema
That being said, I'm actually quite happy with my current solution that uses Proxy
, but if there is a less "hacky" method then I'm all for it.
- 1 What keys are you expecting from union types? All of them? What do you mean by "more plex schemas" (other than unions and intersections)? Please update the question with clear examples of the inputs and outputs you expect. – T.J. Crowder Commented Jan 17, 2024 at 13:35
3 Answers
Reset to default 5In recent zod, You can get them by keyof().options
.
const keys = schema.keyof().options
Try this
schema.keyof()._def.values
Why not have the built-in parser do the work for us...
Note: Won't work for objects in arrays
Simplest solution for extracting the keys of a flat zod schema:
import type { ZodSchema } from 'zod';
export function getZodSchemaFieldsShallow(schema: ZodSchema) {
const fields: Record<string, true> = {};
const proxy = new Proxy(fields, {
get(_, key) {
if (key === 'then' || typeof key !== 'string') {
return;
}
fields[key] = true;
},
});
schema.safeParse(proxy);
return fields;
}
With nesting:
import type { ZodSchema } from 'zod';
type ZodSchemaFields = { [K: string]: ZodSchemaFields | true };
type DirtyZodSchemaFields = { [K: string]: DirtyZodSchemaFields };
const _proxyHandler = {
get(fields: DirtyZodSchemaFields, key: string | symbol) {
if (key === 'then' || typeof key !== 'string') {
return;
}
if (!fields[key]) {
fields[key] = new Proxy({}, _proxyHandler);
}
return fields[key];
},
};
function _clean(fields: DirtyZodSchemaFields) {
const cleaned: ZodSchemaFields = {};
Object.keys(fields).forEach((k) => {
const val = fields[k];
cleaned[k] = Object.keys(val).length ? _clean(val) : true;
});
return cleaned;
}
export function getZodSchemaFields(schema: ZodSchema): ZodSchemaFields {
const fields = {};
schema.safeParse(new Proxy(fields, _proxyHandler));
return _clean(fields);
}
Only thing is, you can't have any keys named then