I have react-hook-form
and zod/v4
for forms. I define zod
validation object, and pass it as resolver to useForm
. zod
marks all fields as required by default and could mark them as optional. I need to explicitly show whether the field is optional or required (add text 'Optional if so'). But zod
scheme does not pass the info to react-hook-form
, therefore this info can't be reached from useController
.
So, I came up with the idea to make a helper function that takes an already defined zod
scheme and returns object that is exactly like the shape of the scheme, but with a boolean value indicating whether the field is required or not.
// input
const schema = z.object({
name: z.string(),
email: z.email().optional(),
address: z.object({
line1: z.string(),
line2: z.string().optional(),
}),
})
// output
{
name: true,
email: false
address: {
line1: true,
line2: false,
},
}
I already came up with this function, but it works only with one level of nesting.
function isOptional(schema: ZodType) {
return schema.safeParse(undefined).success
}
export function getRequiredFields<T extends Record<string, ZodType>>(
schema: ZodObject<T>
) {
const { shape } = schema
const map = {} as Record<keyof T, boolean>
for (const key in shape) {
const field = shape[key]
map[key] = !isOptional(field)
}
return map
}
This is my improved version for any level of nesting:
type RequiredFieldsMap<T> = {
[Key in keyof T]: boolean | RequiredFieldsMap<T[Key]>
}
export function getRequiredFields<T extends Record<string, ZodType>>(
schema: ZodObject<T>
) {
const { shape } = schema
const map = {} as RequiredFieldsMap<T>
for (const key in shape) {
const field = shape[key]
if (field instanceof ZodObject) {
map[key] = getRequiredFields(field)
} else {
map[key] = !isOptional(field)
}
}
return map
}
And it works, at least for two nesting levels, but the typing is wrong. I can't come up with the right types, so I'm seeking help either with this typing or another solution to this problem.
You can use conditional types for proper deep typing
import { ZodObject, ZodTypeAny, ZodOptional, ZodNullable, ZodType, z } from 'zod'
type RequiredFieldsMap<T> = {
[K in keyof T]: T[K] extends ZodObject<infer U>
? RequiredFieldsMap<U>
: boolean
}
function isOptional(schema: ZodTypeAny): boolean {
return schema.safeParse(undefined).success
}
export function getRequiredFields<T extends ZodObject<any>>(
schema: T
): RequiredFieldsMap<T['shape']> {
const shape = schema.shape
const map: any = {}
for (const key in shape) {
const field = shape[key]
map[key] = field instanceof ZodObject
? getRequiredFields(field)
: !isOptional(field)
}
return map
}
This works with nested objects and infers correct RequiredFieldsMap
.