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",...
D
deforestor352d ago
From the Zod docs:
D
deforestor352d ago
GitHub
TypeScript-first schema validation with static type inference
TypeScript-first schema validation with static type inference
D
deforestor352d 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
M
Mir352d 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.
D
deforestor352d ago
ok, let me see if I can do this, hold on
M
Matvey352d 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
M
Mir352d ago
Okay one more small addition to it, how will it work if keys does not match the enums? 👀
D
deforestor352d ago
yeah that seems to work well
M
Mir352d ago
.refine(d => { if(d.someKey !== undefined) return false })
.refine(d => { if(d.someKey !== undefined) return false })
?
D
deforestor352d 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
M
Mir352d ago
so, add a separate refine for each fields?
D
deforestor352d 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
M
Matvey352d 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);
D
deforestor352d ago
Yeah, I think in general it would just be better to match the enum So you can oneline it like that
M
Mir352d ago
This seems like the best solution overall! 💯 🙏 You guys are awesome!! TYSM!!! ❤️
N
naz6352352d 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
N
naz6352352d 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 🙂
D
deforestor350d ago
That is actually amazing, thank you for that naz
M
Mir329d ago
This is actually is the best way to handle this problem! tysm!! ❤️
D
deforestor299d 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
More Posts
Is it currently possible to use clerk auth with NextJS' 13.4 server actions?I know it's currently in alpha, but I'd like to know if there is any way to use it right now. LibraTesting on project with @t3-oss/env using vitestWhen I try to write unitest on brand new create-t3-app ("initVersion": "7.13.0") with vitest installT3 for a chat appAlthough I don't remember the exact video now. I remember Theo saying that the t3 stack would not beHow do I use a tRPC procedure outside a react componentHello guys, I made a simple router which will give me similar tags from my tags model from prisma `Syntax help for useQuery with no inputshi, i have two queries in my component, however on my first query I can't pass in refetchOnMount? anAlternative to Planetscale for multiple smaller appsHi While creating our latest tool, I used the tRPC + Planetscale + Clerk stack and fell in love witRemoving unused importsA rather annoying and controversial thing that was built into version 7.3.2. As far as I can see, tvariable initialized useState as true but the state is falsehasReaction is true, but liked is false error gets printed multiple times const hasReaction = poscreate-t3-turbo monorepo: How to access user session to packages/db?I would like to access some props from user's session to **prisma middleware** i.e. in below exampleCreating an update mutation with tRPCHi everyone, I'm trying to use an update mutation in this way: ```ts export const beerRouter = creatCreate and Download PDFI have a web app that allows users to edit a table including drag/dropping rows, change row colors, Is it possible to pass a zod schema as a prop?I have this ```ts interface Props { schema: ZodSchema; children: React.ReactNode; defaulLoading state of a `'use server'` componentIs it possible to have a loading state for e.g. a form submission using the new `use server` directiHow do you self host your projects in a vpswithout relying on any cpanel or vercel, i have a vps and i usually move the project using git and Geniune question: php vs server componentsI love nextjs. I hear a lot of comments about nextjs just reinvented php. Can someone clarify thingsHow do you fetch an svg from s3 into an angular app?I tried several things, but each time the svg isn't displayed.What's this errorit was working till yesterday then i added stateDistrict to address model then i started to get thisPrisma find many undefinedHello, In my other projet when i do `prisma.resource.findMany({select: ...})` the type is `{id: strESLint to force @types in dev-dependenciesWe recently had an outage coz someone installed the @types/[package-name] in dependencies, but neverUpdating existing T3 with Supabase Auth from Next AuthBackground: - Launched a T3 app with Next Auth. - Currently got 3000 users. - Launching an app ve