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
Require one of two fields #61
Comments
The But for this scenario I think it's overfill. I would implement this as a custom refinement/validator on the outer object: const myObject = z
.object({
first: z.string(),
second: z.string(),
})
.partial()
.refine(
data => !!data.first || !!data.second,
'Either first or second should be filled in.',
); The The const myObject = z
.object({
first: z.string(),
second: z.string(),
})
.partial()
.refine(
data => data.first || data.second,
'Either first or second should be filled in.',
); Let me know if I misunderstood the scenario. 👍 |
Perfect - I was able to get this working. I had to define the refine twice though. I'm using I was looking at refine in the docs hoping it could solve my problem but didn't see any examples of using it on the zod object itself and not the properties. I'm already using refine a lot because of mui (going to open a new issue about this). Anyways, thanks for the help. |
I'll try to clarify that the |
@KolbySisk thanks for pointing out the fact that you had to define the refinement twice. That was a bug. With zod@1.7 or later, if you |
Would you mind sharing a sample of this @KolbySisk ? I'm having a bit of trouble with it.. |
This reverts commit 52bffa1. This change actually introduced a bug where form validation started to fail if form was set to "indefinte" and "validityEnd" date was missing. Proper way to tackle the issue would be introducing some kind of conditional validation e.g. by using `partial()` and `refine()` methods from `zod` as instucted here: colinhacks/zod#61 Example implementation would look something like this: ``` export const schema = z .object({ priority: z.nativeEnum(Priority), validityStart: z .string() .min(1) .regex(/[0-9]{4}-[0-9]{2}-[0-9]{2}/), }) .merge( z .object({ validityEnd: z.string().regex(/[0-9]{4}-[0-9]{2}-[0-9]{2}/), indefinite: z.boolean(), }) .partial() .refine((data) => !!data.indefinite || !!data.validityEnd), ); ``` The problem is that zod won't let us to `merge()` schemas after `refine()` has been called (https://giters.com/colinhacks/zod/issues/597), and causes problems in all places where ConfirmationForm has been used.
This reverts commit 52bffa1. Reverted change actually introduced a bug where form validation started to fail if form was set to "indefinte" and "validityEnd" date was missing. Proper way to tackle the issue would be introducing some kind of conditional validation e.g. by using `partial()` and `refine()` methods from `zod` as instucted here: colinhacks/zod#61 Example implementation would look something like this: ``` export const schema = z .object({ priority: z.nativeEnum(Priority), validityStart: z .string() .min(1) .regex(/[0-9]{4}-[0-9]{2}-[0-9]{2}/), }) .merge( z .object({ validityEnd: z.string().regex(/[0-9]{4}-[0-9]{2}-[0-9]{2}/), indefinite: z.boolean(), }) .partial() .refine((data) => !!data.indefinite || !!data.validityEnd), ); ``` The problem is that zod won't let us to `merge()` schemas after `refine()` has been called (https://giters.com/colinhacks/zod/issues/597), and causes problems in all places where ConfirmationForm has been used.
This reverts commit 52bffa1. Reverted change actually introduced a bug where form validation started to fail if form was set to "indefinte" and "validityEnd" date was missing. Proper way to tackle the issue would be introducing some kind of conditional validation e.g. by using `partial()` and `refine()` methods from `zod` as instucted here: colinhacks/zod#61 Example implementation would look something like this: ``` export const schema = z .object({ priority: z.nativeEnum(Priority), validityStart: z .string() .min(1) .regex(/[0-9]{4}-[0-9]{2}-[0-9]{2}/), }) .merge( z .object({ validityEnd: z.string().regex(/[0-9]{4}-[0-9]{2}-[0-9]{2}/), indefinite: z.boolean(), }) .partial() .refine((data) => !!data.indefinite || !!data.validityEnd), ); ``` The problem is that zod won't let us to `merge()` schemas after `refine()` has been called (https://giters.com/colinhacks/zod/issues/597), and causes problems in all places where ConfirmationForm has been used.
To anyone coming across this issue looking for something similar to yup's when option, I wanted the error to appear on the field where the problem actually is not just at the top level as in @colinhacks example above and I thought I was at a dead end. I was wrong! With the superRefine method, you can add a path to the addIssue call. E.g.
|
Feel the need to comment on this as I keep running into cases this is needed. I have large schema that needs to require one or another field. A nested partial Zod object with superRefine works for this (as shown above). 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: Upon "ValueA" then There are also other required fields within the schema that should remain required without explicitly checking them in something like Is there any way to do this? Any plans for something like |
One month! Faster mtf! |
Can we reopen this issue? I don't think using super refine is a suitable solution for complex form validation. Making all fields optional and checking them in a superRefine function doesn't make sense for several use cases I'm currently working on. I think @ckifer summed up the issue perfectly. |
@colinhacks thoughts on re-opening this or creating a new issue? It seems as though there are complexities with implementing something like this and understandably so with the dynamic typing that would have to go on in order to keep zod as type safe as it can be while allowing zod to behave in the same way it does currently. zod isn't yup and I don't think we expect it to be, but there are a lot of valid use-cases for these types of conditional validations/requirements and yup does do it quite well (and with recent yup beta versions have much better TS support). Just the response to my comment above I think warrants another look at the problem. Edit: created #1394 |
I think it should be a new issue because the original issue as posted was solved. |
Hey @colinhacks, how would one make that work when I want to use this for more advanced strings? For example, I have a registration form with three fields: Password is always required. At least one of username or email should be required. I thought I could use a zod object as you described here. However, that doesn't work when you add const schema = z
.object({
email: z.string().email().trim(),
username: z.string().regex(/^[a-zA-Z_]+$/).trim(),
password: z.string().min(8, { message: 'Password must be at least 8 characters' }),
})
.partial({
email: true,
username: true,
})
.refine(
data => data.email || data.username,
'Email or username required.',
); This one will still complain that an empty string "" is not a valid email / username, even when the other one is entered. |
I came up with a hack(?). It works, but it's not really beautiful imo This schema allows a registration of a user with either or both username/email, and a password. const schema = z
.object({
email: z.string().email().trim(),
username: z.string().regex(/^[a-zA-Z_]+$/).trim(),
password: z.string().min(8, { message: 'Password must be at least 8 characters' }),
})
.or(
z.object({
username: z.string().regex(/^[a-zA-Z_]+$/).trim(),
password: z.string().min(8, { message: 'Password must be at least 8 characters' }),
})
).or(
z.object({
email: z.string().email().trim(),
password: z.string().min(8, { message: 'Password must be at least 8 characters' }),
})
) |
I solve my issue with this solution const FirstSchema = z.object({
email: z.string().email(),
});
const SecondSchema = z.object({
username: z.string().regex(/^[a-zA-Z_]+$/),
});
const MainSchema = z
.object({
password: z
.string()
.min(8, { message: 'Password must be at least 8 characters' }),
})
.merge(FirstSchema.partial())
.merge(SecondSchema.partial())
.superRefine((data, ctx) => {
if (data.email) {
const result = FirstSchema.safeParse(data);
if (!result.success) {
result.error.errors.forEach((issue) => ctx.addIssue(issue));
}
} else if (data.username) {
const result = SecondSchema.safeParse(data);
if (!result.success) {
result.error.errors.forEach((issue) => ctx.addIssue(issue));
}
} else {
ctx.addIssue({
code: 'custom',
path: ['username', 'email'],
message: 'email or username is required',
});
}
}); |
EDIT: THIS DOESN'T WORK If you're willing to repeat yourself, this solution provides accurate typing; it's great if you're using tRPC. const myObject = z
.object({
first: z.string(),
second: z.string(),
})
.partial()
.merge(
z.object({first: z.string()})
.or(z.object({second: z.string()}))
); |
@serg06 do you have an example on how you used it with tRPC? |
Nice catch @gradam , it doesn't work after all. :( |
has anyone tried a solution with arrays ? Have at least one item in array for one of two fields ? |
as a variant if your need set some paths const myObject = z
.object({
first: z.string(),
second: z.string(),
third: z.string(),
}).superRefine((data, ctx) => {
if(!data.thrid){
ctx.addIssue({
code: 'custom',
path: ['first'],
message: 'error message',
});
ctx.addIssue({
code: 'custom',
path: ['second'],
message: 'error messaget',
});
}
}); |
A type-safe approach: const schema = z.object({
email: z.string().optional(),
username: z.string().optional(),
// other properties here
})
.and(z.union([
z.object({email: z.undefined(), username: z.string()}),
z.object({email: z.string(), username: z.undefined()}),
z.object({email: z.string(), username: z.string()}),
], {errorMap: (issue, ctx) => ({message: "Either email or username must be filled in"})})); A benefit of this approach is that parsed object is well-typed. Thus, following code doesn't cause type errors: const obj = schema.parse({
email: "foo",
});
const emailOrUsername = obj.email ?? obj.username;
emailOrUsername.length; // emailOrUsername is "string", not "string | undefined" |
Answer of @frsyuki works well if there are only 2 fields, however if there more ,then this solution will be overcomplicated.
And then it is possible to customise checks for your task |
I've handled this situation with
This way the request should have |
This is type-safe, but the validation error message is not user-friendly if a client is not TypeScript. {
"email":["Required"],
"username":["Required"],
} And if both fields are present, {
"email": ["Expected undefined, received string"],
"username": ["Expected undefined, received string"],
} It appears that |
function oneOf<A, K1 extends Extract<keyof A, string>, K2 extends Extract<keyof A, string>, R extends A & (
Required<Pick<A, K1>> & { [P in K2]: undefined } |
Required<Pick<A, K2>> & { [P in K1]: undefined }
)>(key1: K1, key2: K2): ((arg: A, ctx: RefinementCtx) => arg is R) {
return (arg, ctx): arg is R => {
if ((arg[key1] === undefined) === (arg[key2] === undefined)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Either ${key1} or ${key2} must be filled, but not both`,
});
return false;
}
return true;
};
}
const schema = z.object({
email: z.string().optional(),
username: z.string().optional(),
// other properties here
}).superRefine(oneOf("email", "username")); This worked for me. It is type-safe and the error message is user-friendly. |
Doesn't the use of |
Using |
Similar to this Yup issue: jquense/yup#176
I'd like to conditionally validate at least one of n values are set. Yup and Joi solve this problem with a
.when
method.Does Zod currently have a solution to this problem?
The text was updated successfully, but these errors were encountered: