-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature: Conditional Validation Similar to yup.when() #1394
Comments
This is currently the only problem I have with zod - I have to do conditional field validation in superRefine which is not the problem but then if I want to use that schema in other places with I think that something like |
Another possible solution for this issue is Joi's const schema = z.object({
field1: z.string(),
field2: z.number(),
}).xor(['field1', 'field2']); // only one of these fields can be present/non-nullish But Some other possible solutions from different libraries:
|
+1 |
@colinhacks almost at 90 +1s here. Is something like this even possible in zod? I know you have mentioned elsewhere that it isn't trivial. Anything that you can comment on that might give us an idea if it could even be a feature in the future? |
I'm joining this thread to provide more requirements than just requiring My use case is that I have an optional I have achieved this in Yup thanks to |
@deniskabana can you provide a code snippet and how you'll like it to be ideally? |
I wrote a bit of untested code using Yup for you since I have never worked with Zod before. I wanted to dive in deep today - I read the official website's docs and a few tutorials, took a look at the APIs and then I started googling for my exact use-case. Yup.date()
.when(["startDate", "endDate"], ([startDate, endDate]: [Date, Date], schema) => {
if (!startDate && !endDate) return schema;
if (startDate && !endDate) return schema.required(errors.validation.dateRange.required);
if (!startDate && endDate) return schema.required(errors.validation.dateRange.required);
return schema.required().test("date-range-test", errors.validation.dateRange.startBeforeEnd, () => {
// Check if both are dates object
if (!("getMonth" in startDate) || !("getMonth" in endDate)) return false;
const monthFrom = startDate.getMonth();
const monthTo = endDate.getMonth();
if (monthFrom < monthTo) return true;
if (monthFrom > monthTo) return false;
return startDate.getDate() < endDate.getDate();
});
}); This is verbose, has no type checkings, but it works (I suppose - I haven't tested this specific snippet). |
@deniskabana definitely a valid case imo. Ideally we would have access to the rest of the schema, access to some sort of context external to the schema (if passed), and would be able to change the validation on more than just one field at a time while maintaining the validations that have been untouched. The biggest problem with |
This isn't going to happen in Zod, sorry folks. I find the
This was never necessary, I was just trying to simplify the example code I provided in #61. You need to make the two relevant fields optional then add an appropriate refinement. const myObject = z
.object({
first: z.string().optional(),
second: z.string().optional(),
})
.refine(
data => !!data.first || !!data.second,
'Either first or second should be filled in.',
); If you want slightly more accurate union typing, declare a base type, create your variants, and union them together. const baseObject = z.object({
// whatever
});
const mySchema = baseObject
.extend({ first: z.string() })
.or(baseObject.extend({ second: z.string() })) To solve the case where different fields are required based on an enum key: z.object({
key: z.enum(['first', 'second']),
first: z.string().optional(),
second: z.string().optional()
}).refine(val => {
if(val.key === 'first') return !!val.first;
if(val.key === 'second') return !!val.second;
return true;
}) There's no scenario I see where a |
@colinhacks Thank you for your response! I understand your point about Yup's But now as I'm working with the date range values as in the example I provided, I don't see how to make a refinement easy and readable, that does the following:
I can validate this easily on the server with custom logic. But it would make much more sense UX-wise if this was easier to implement on the frontend as well for immediate user feedback. |
Thanks for the response @deniskabana. The problem is that these refinements shouldn't live inside the const schema = z
.object({
startDate: z.date().optional(),
endDate: z.date().optional(),
actualDate: z.date(),
})
.refine((val) => {
if (!val.startDate && !val.endDate) return true;
if (!val.startDate || !val.endDate) return false;
return val.actualDate >= val.startDate && val.actualDate <= val.endDate;
}); Note that you actually have typing on You can also define this refinement as a separate function and drop it into multiple schemas. const dateRangeValidator = (val: {
startDate?: Date;
endDate?: Date;
actualDate: Date;
}) => {
if (!val.startDate && !val.endDate) return true;
if (!val.startDate || !val.endDate) return false;
return val.actualDate >= val.startDate && val.actualDate <= val.endDate;
};
const schema = z
.object({
startDate: z.date().optional(),
endDate: z.date().optional(),
actualDate: z.date(),
})
.refine(dateRangeValidator); |
@colinhacks Thank you for that response with examples, that really cleared the situation for me! To further clarify for you and others, Yup does check the date type, so the actual schema for the example I have shown would be The second example of yours clears this very well. There is no need to use superRefine other than for the error messages. Have a great day! |
Thanks for the response @colinhacks! I wasn't too hopeful, but thank you for more example and clarity. Much appreciated! |
It would be nice if there was a way to pass in context or something to refine. For another example, I have one validator that needs to make a server request (to check if the name has been used before), but that request needs another that I have access to when I create the form. But for the sake of reusability, I want to be able to make that validator a function that can be used in different contexts. |
@leximarie you can create a function that accepts some arguments and returns a function for the refine/superRefine. Usage example: z.string().superRefine(myValidator('some arg')) |
@asologor but where do you pass in the argument? (I'm attempting to use zod with react-hook-form) |
@leximarie dunno. You said you have access to that other argument when you create a form. Just think whether it's suitable in your case. Maybe it's not. You don't have too many options with Zod here. |
@colinhacks it seems as though the the From the docs: const passwordForm = z
.object({
password: z.string(),
confirm: z.string(),
})
.refine((data) => data.password === data.confirm, {
message: "Passwords don't match",
path: ["confirm"], // <-- path of error
}); Or the Discriminated union? const myUnion = z.discriminatedUnion("status", [ // <--
z.object({ status: z.literal("success"), data: z.string() }),
z.object({ status: z.literal("failed"), error: z.instanceof(Error) }),
]); |
@colinhacks I strongly disagree, can you please elaborate on this? Stripe has many apis that have mutually exclusive fields and subfields that are dependant on the value of sibling fields. For example, to create a Price object, one of I realize this could be achieved using refine or superRefine but that doesn't feel like a great solution. That's what I've just done and why I'm commenting here. It makes it harder to reuse and extend our schemas because refinements must be kept separate. It also doesn't contribute to a pit of success IMHO. |
Similar to this Yup issue: jquense/yup#176 and creating a new issue out of #61.
Many users of zod would like to do conditional requirement or validation of fields based on either fields passed as context to zod, or based off of the value of another schema property. This is common to do in form validations.
The closed issue #61 addresses how to do conditional validation with
superRefine
. This works well enough in small cases, but many folks have large complex schemas and we would like to keep the existing validation on an object i.e. not have to make the entire object partial in order for conditional requirement to work.This issue stems from the reaction to my comment #61 (comment)
Pasting from the comment as my case is the same:
I have large schema that needs to require one or another field. A nested partial Zod object with
superRefine
works for this.What I want to be able to do however, is to do conditional requirement on 1 or 2 fields without making the entire object partial and while having access to all of the fields in the object.
Ex:
I have a required enum that has two values: "ValueA" and "ValueB"
Upon "ValueA" then someOtherFieldA is required. Upon "ValueB" then someOtherFieldB is required.
There are also other required fields within the schema that should remain required without explicitly checking them in something like
superRefine
.The reason I chose zod over yup was TypeScript support, but in yup's beta versions TS support has improved a lot. Not having a functionality like this is a big blocker for me wanting to keep using zod. Thanks!
The text was updated successfully, but these errors were encountered: