Open OnkelTem opened 1 year ago
Hey @OnkelTem, I was actually just working on a method that might help you out here. Try this out with discriminatedUnion
s and see if it works for you (I demoed with your use cases from your sandbox):
import z from "zod";
const group1Schema = z.object({
firstName: z.string(),
lastName: z.string()
});
const group2Schema = z.object({
email: z.string().email(),
address: z.string()
});
const group1Validator = z.object({
group1: group1Schema,
type: z.literal('groupOne') // this could be true/false/whatever you want
});
const group2Validator = z.object({
group1: group1Schema,
group2: group2Schema,
type: z.literal('groupTwo')
});
const schema = z.discriminatedUnion('type', [group1Validator, group2Validator])
type Schema = z.infer<typeof schema>;
schema.parse({
group1: {
firstName: "value",
lastName: "value"
},
type: 'groupOne'
});
schema.parse({
group1: {
firstName: "value",
lastName: "value"
},
group2: {
email: "value@domain.com",
address: "Some address..."
},
type: 'groupTwo'
})
schema.parse({
group1: {
firstName: "value",
lastName: "value"
},
group2: {
email: "value@domain.com",
address: "Some address..."
},
type: 'groupOne'
})
console.log('success')
EDIT: re this
the group2 should be discarded
zod will strip out unrecognized keys by default so you should be good here
@maxArturo Unfortunately I think that solution will start to break down pretty quickly once you start adding more groups, because you'll need to define a "validator" for every possible combination of optional parts.
I had to do something similar, where my schema could optionally be extended with extra bits and I solved it using intersections with unions. The short of it is that the optional part is represented as a union of nothing (or in this case, useGroup2: false) with the full object (useGroup2: true, group2: {...}).
const group1Schema = z.object({
group1: z.object({
firstName: z.string(),
lastName: z.string(),
}),
});
const group2Schema = z.union([
z.object({
useGroup2: z.literal(false),
}),
z.object({
useGroup2: z.literal(true),
group2: z.object({
email: z.string().email(),
address: z.string(),
}),
}),
]);
const fullSchema = group1Schema.and(group2Schema);
EDIT:
When you want to check for the existence of group2 in this example, you would do something like:
if (parsedData.useGroup2) {
// Type inference kicks in and you now have access to parsedData.group2
console.log(parsedData.group2.email);
}
@Roundaround Yep that makes sense! That's a cleaner way of doing it for sure. @OnkelTem I'd go with their suggestion.
Thank you folks for your answers.
I don't think those discriminated/union methods would work at all. Because you need to initialize the form, right? And you cannot do this with discriminators or unions.
Here is a sandbox with the @Roundaround 's approach:
https://codesandbox.io/s/proud-thunder-yg1xdu?file=/src/App.tsx
So the idea is to still keep some default values for the form, even if useGroup2
is false
.
I'm not familiar with this form library, but from what I gather, everything in the defaultValues object gets sent in the submit invocation, right? So it should only reflect your actual default (i.e. useGroup2: false and no group2 defined). If you still want to add default values to those dynamic inputs, it looks like you can define them inline on the input itself via the defaultValue
prop instead of the big object at the beginning. That being said, if your default value actually is just empty string, I'm pretty sure you can just omit it entirely.
@OnkelTem Has this issue been resolved? Or do you have any other questions?
I'd like to close this issue if there are no further questions.
I have a somewhat similar issue that i thought was straight forward in Zod. I thought I was going to see something like this for the above question (just changed a few things to illustrate the straightforwardness of what I was thinking):
schema = z.object({
firstName: z.string(),
lastName: z.string(),
moreInfo: z.boolean(),
email: z.string().email().when(z.object({ moreInfo: z.literal(true) })),
address: z.string().when(z.object({ moreInfo: z.literal(true) })),
})
// usage is simple, just use parse as usual.
schema.parse(data)
I thought Zod could provide a conditional of some sort that way. It's readable and cheap to write.
email
and address
are optional, but are changed to required when that inner schema passes.
The above could be done in a modular way:
const moreInfoSchema = z.object({ moreInfo: z.literal(true) });
const schema = z.object({
firstName: z.string(),
lastName: z.string(),
moreInfo: z.boolean(),
email: z.string().email().when(moreInfoSchema),
address: z.string().when(moreInfoSchema),
})
// usage is simple, just use parse as usual.
schema.parse(data)
In addition to when
, there could also be otherwiseWhen
and otherwise
; these are just if
, else-if
and else
and should work in a similar fashion.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
Hey guys, you are trying to achieve something similar to #1394, but @colinhacks has expressed his understandable concerns (https://github.com/colinhacks/zod/issues/1394#issuecomment-1346013265) with something like .when
.
I have the same problem though and find the proposed solutions pretty complicated so far (given that the scenario comes up pretty often in more complex forms). I would also like to find an elegant way of defining conditional parts in the schema, but this seems pretty hard right now.
Just for reference: #1422 could also be interesting in this context.
I think what @emahuni says makes a lot of sense, although passing a function would be easier to handle and a more elegant solution.
schema = z.object({
firstName: z.string(),
lastName: z.string(),
moreInfo: z.boolean(),
email: z.string().email().when((data) => data.moreInfo),
address: z.string().when((data) => data.moreInfo && data.firstName === 'Joe'), // would also allow some more complex logic to be entered.
})
schema.parse(data)
I also think that using isRequired
instead of when
is more explicit to the objective of the function.
schema = z.object({
firstName: z.string(),
lastName: z.string(),
moreInfo: z.boolean(),
email: z.string().email().isRequired((data) => data.moreInfo),
address: z.string().isRequired((data) => data.moreInfo && data.firstName === 'Joe'),
})
schema.parse(data)
You can enable/disable parts of the schema dynamically using z.lazy
.
import { z } from 'zod';
const myState = {
moreInformation: false,
};
const mySchema = z.lazy(() => {
return z.object({
firstName: z.string(),
lastName: z.string(),
...(myState.moreInformation ? {
email: z.string().email(),
address: z.string(),
} : {})
});
});
myState.moreInformation = false;
console.log(
mySchema.safeParse({
firstName: "Jane",
lastName: "Doe",
}).success ? "OK" : "Not OK",
) // OK
myState.moreInformation = true;
console.log(
mySchema.safeParse({
firstName: "Jane",
lastName: "Doe",
}).success ? "OK" : "Not OK",
) // Not OK
myState.moreInformation = true;
console.log(
mySchema.safeParse({
firstName: "Jane",
lastName: "Doe",
email: "jane@doe.com",
address: "123 Zod St.",
}).success ? "OK" : "Not OK",
) // OK
hey @ealmansi thanks, thats good to know
I use the z.discriminatedUnion
function for this kind of situations too
Like this:
const schema = z.discriminatedUnion('status', [
z.object({ status: 'status one' }).extend(statusOneSchema),
z.object({ status: 'status two' }).extend(statusTwoSchema),
z.object({ status: 'status three' }).extend(statusThreeSchema),
z.object({ status: 'status four' }).extend(statusFourSchema),
])
Here is what I like to do:
const userSchema = z.object({
firstName: z.string(),
lastName: z.string(),
email: z.optional(z.string().email()),
address: z.optional(z.string()),
});
const requiredUserSchema = userSchema.extend({
email: userSchema.shape.email.unwrap(),
address: userSchema.shape.address.unwrap(),
});
This way, I can define all the rules in a single place, and just unwrap the optional fields if I need them to be required in another place.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
I have quite a regular situation when one edits more than one group of fields on a form. And depending on some conditions (f.e. checkbox states), those groups can be displayed/hidden and correspondingly - should be validated/skipped.
Consider this simple form:
So we enter first and last names, and if we check "More information", a new part appears with two additional fields: email and address.
Here is a sandbox: https://codesandbox.io/s/zod-problem-forked-r7xmcm?file=/src/index.ts
There are two base schemas:
And one with their combination for the form:
The problem is that now it's not possible to submit this form w/o the second (optional) part.
There is no form in this sandbox (to keep it minimal) but there is the
defaultValues
const which imitates the corresponding property of the ReactHookFormuseForm()
hook.As you see, you cannot even initialize such a form with empty values because they get validated and validation doesn't pass. Of course, this is expected behaviour, granted how I declared the schemas.
Hence my question: how to conditionally enable/disable parts of the schema?
This form should pass to its submit handler the following:
1) if only first and last name entered and the checkbox is off:
2) if all values are entered and the checkbox is on:
3) IMPORTANT if all values are entered, but the checkbox is OFF, the group2 should be discarded, and the result should be equal to the 1st reply:
If you have any other ideas on how to solve such tasks, please share.