Zod conditional required/optional validation

I am using React Hook Form and Zod. I want make some fields optional/required based on other inputs. Here's an example:
const schema = z.object({
type: z.enum(['image', 'video', 'image-and-video', 'other']),
image: z.custom<File>(),
video: z.custom<File>(),
otherNonMatchingKey: z.custom<File>(),
})
const schema = z.object({
type: z.enum(['image', 'video', 'image-and-video', 'other']),
image: z.custom<File>(),
video: z.custom<File>(),
otherNonMatchingKey: z.custom<File>(),
})
I want image to be required only when type: image is selected and vice-versa.
Solution:
if you have a more then two options this solution will be nicer ```ts const enumToField = { video: "videoFile",...
Jump to solution
20 Replies
deforestor
deforestor2y ago
From the Zod docs:
deforestor
deforestor2y ago
GitHub
TypeScript-first schema validation with static type inference
TypeScript-first schema validation with static type inference
deforestor
deforestor2y ago
In essence, you just have to use z.refine and check for the the conditions you want But in any case, both will have to be optional for this to work, I believe
Mir
MirOP2y ago
I read it already. If you can provide an example that would be appreciated. I want image to be required only when type: image is selected.
deforestor
deforestor2y ago
ok, let me see if I can do this, hold on
Matvey
Matvey2y ago
Something like this?
const schema = z.object({
type: z.enum(['image', 'video']),
image: z.custom<File>(),
video: z.custom<File>(),
}).refine(d => d[d.type] !== undefined)
const schema = z.object({
type: z.enum(['image', 'video']),
image: z.custom<File>(),
video: z.custom<File>(),
}).refine(d => d[d.type] !== undefined)
it will error if data[data.type] is undefined
Mir
MirOP2y ago
Okay one more small addition to it, how will it work if keys does not match the enums? 👀
deforestor
deforestor2y ago
yeah that seems to work well
Mir
MirOP2y ago
.refine(d => { if(d.someKey !== undefined) return false })
.refine(d => { if(d.someKey !== undefined) return false })
?
deforestor
deforestor2y ago
Then I think you could do manually?
.refine((d) => d.type === "image" && d.imageFile !== undefined)
.refine((d) => d.type === "video" && d.videoFile !== undefined);
.refine((d) => d.type === "image" && d.imageFile !== undefined)
.refine((d) => d.type === "video" && d.videoFile !== undefined);
I tested here and that seems to work
Mir
MirOP2y ago
so, add a separate refine for each fields?
deforestor
deforestor2y ago
Sorry, I meant
.refine((d) => (d.type === "image" && d.imageFile !== undefined) || (d.type === "video" && d.videoFile !== undefined));
.refine((d) => (d.type === "image" && d.imageFile !== undefined) || (d.type === "video" && d.videoFile !== undefined));
No, that fails because it checks one, passes, then checks the next and fails. My bad. The second solution should work though
Solution
Matvey
Matvey2y ago
if you have a more then two options this solution will be nicer
const enumToField = {
video: "videoFile",
audio: "audioFile",
...
};

schema
.refine((d) => d[enumToField[d.type]] !== undefined);
const enumToField = {
video: "videoFile",
audio: "audioFile",
...
};

schema
.refine((d) => d[enumToField[d.type]] !== undefined);
deforestor
deforestor2y ago
Yeah, I think in general it would just be better to match the enum So you can oneline it like that
Mir
MirOP2y ago
This seems like the best solution overall! 💯 🙏 You guys are awesome!! TYSM!!! ❤️
naz6352
naz63522y ago
There is also z.discriminatedUnion https://zod.dev/?id=discriminated-unions So you could do something like
const schema = z.discriminatedUnion("type", [
z.object({ type: z.literal("image"), image: z.custom<File>() }),
z.object({ type: z.literal("video"), video: z.custom<File>() }),
]);
const schema = z.discriminatedUnion("type", [
z.object({ type: z.literal("image"), image: z.custom<File>() }),
z.object({ type: z.literal("video"), video: z.custom<File>() }),
]);
GitHub
TypeScript-first schema validation with static type inference
TypeScript-first schema validation with static type inference
naz6352
naz63522y ago
And to include otherNonMatchingKey I've done it before via this sort of pattern
const BaseSchema = z.object({ otherNonMatchingKey: z.custom<File>() })

const VideoSchema = z
.object({ type: z.literal('video'), video: z.custom<File>() })
.merge(BaseSchema);

const ImageSchema = z
.object({ type: z.literal('image'), folder: z.custom<File>() })
.merge(BaseSchema);

const schema = z.discriminatedUnion('type', [
VideoSchema,
ImageSchema,
]);
const BaseSchema = z.object({ otherNonMatchingKey: z.custom<File>() })

const VideoSchema = z
.object({ type: z.literal('video'), video: z.custom<File>() })
.merge(BaseSchema);

const ImageSchema = z
.object({ type: z.literal('image'), folder: z.custom<File>() })
.merge(BaseSchema);

const schema = z.discriminatedUnion('type', [
VideoSchema,
ImageSchema,
]);
I used to do it via .refine too but then I discovered that if you do it this way the type of z.infer<typeof schema> is
{
type: "video";
otherNonMatchingKey: File;
video: File;
} | {
type: "image";
folder: File;
otherNonMatchingKey: File;
}
{
type: "video";
otherNonMatchingKey: File;
video: File;
} | {
type: "image";
folder: File;
otherNonMatchingKey: File;
}
which is very nice 🙂
deforestor
deforestor2y ago
That is actually amazing, thank you for that naz
Mir
MirOP2y ago
This is actually is the best way to handle this problem! tysm!! ❤️
deforestor
deforestor2y ago
Josh tried coding just released a video that shows this behavior very well: https://www.youtube.com/watch?v=9i38FPugxB8 it's not in zod, naz's solution is essentially the same, but in zod
Josh tried coding
YouTube
How Did I Not Know This TypeScript Trick Earlier??!
This TypeScript trick to conditionally include types is so useful, especially for React props. I've asked myself how this is done many times before, and it's just really cool to learn something so simple yet useful. -- links Discord: https://discord.gg/4vCRMyzgA5 My GitHub: https://github.com/joschan21
Want results from more Discord servers?
Add your server