Skip to content
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

Closed
KolbySisk opened this issue Jun 5, 2020 · 26 comments
Closed

Require one of two fields #61

KolbySisk opened this issue Jun 5, 2020 · 26 comments

Comments

@KolbySisk
Copy link

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?

@colinhacks
Copy link
Owner

colinhacks commented Jun 5, 2020

The when method in Yup is really interesting, and I'll look into implementing something similar eventually.

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 .partial makes all the properties optional.

The !! is currently required because the .refine method expects a boolean return type. If you update to zod@1.6.2 I just pushed a change such that any can be returned from a refinement check (and any truthy value will pass). This enables a slightly more readable version:

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. 👍

@KolbySisk
Copy link
Author

Perfect - I was able to get this working. I had to define the refine twice though. I'm using .merge to create a new schema using the original with the refine, and had to add the same refine to the new schema. But it works great thank you.

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.

@colinhacks
Copy link
Owner

colinhacks commented Jun 5, 2020

I'll try to clarify that the refine method is applicable to all Zod schemas. I should probably rewrite the readme in a more structured way, similar to Yup.

@colinhacks
Copy link
Owner

@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 .merge two object schemas, the result inherits the refinements of its parents (as it should be).

@justinnoel
Copy link

Perfect - I was able to get this working. I had to define the refine twice though. I'm using .merge to create a new schema using the original with the refine, and had to add the same refine to the new schema. But it works great thank you.

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.

Would you mind sharing a sample of this @KolbySisk ? I'm having a bit of trouble with it..

tuokait pushed a commit to HSLdevcom/jore4-ui that referenced this issue Feb 14, 2022
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.
tuokait pushed a commit to HSLdevcom/jore4-ui that referenced this issue Feb 14, 2022
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.
tuokait pushed a commit to HSLdevcom/jore4-ui that referenced this issue Feb 15, 2022
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.
@maddijoyce
Copy link

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.

const myObject = z
  .object({
    first: z.string(),
    second: z.string(),
  })
  .partial()
  .superRefine((data, ctx) => {
     if (!data.first && !data.second) {
       ctx.addIssue({
         code: z.ZodIssueCode.custom,
         path: ["second"],
         message: "Second should be set if first isn't",
       });
     }
  });

@ckifer
Copy link

ckifer commented Jul 1, 2022

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:
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.

Is there any way to do this? Any plans for something like .when in Yup?

@DrBronsy
Copy link

DrBronsy commented Aug 1, 2022

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: 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.

Is there any way to do this? Any plans for something like .when in Yup?

One month! Faster mtf!

@alex-udisc
Copy link

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.

@ckifer
Copy link

ckifer commented Aug 23, 2022

@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

@ghost
Copy link

ghost commented Sep 3, 2022

I think it should be a new issue because the original issue as posted was solved.

@VIEWVIEWVIEW
Copy link

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.',
  );

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: username, email, password.

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 .email or .regex to the mix:

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.

@VIEWVIEWVIEW
Copy link

VIEWVIEWVIEW commented Oct 4, 2022

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' }),
    })
  )

@mohamad-xtouch
Copy link

mohamad-xtouch commented Oct 31, 2022

I solve my issue with this solution
This is work if you have conditional validation based on some field in the main schema.

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',
      });
    }
  });

@serg06
Copy link

serg06 commented Dec 30, 2022

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()}))
  );  

@gradam
Copy link

gradam commented Jan 3, 2023

@serg06 do you have an example on how you used it with tRPC?
In my case TypeScript is not happy to accept ZodUnion in .input() in tRPC.

@serg06
Copy link

serg06 commented Jan 3, 2023

Nice catch @gradam , it doesn't work after all. :(

@dheysonalves
Copy link

has anyone tried a solution with arrays ? Have at least one item in array for one of two fields ?

@Dmitry-Titov-K
Copy link

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',
      });
     }
  });

@frsyuki
Copy link

frsyuki commented May 4, 2023

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"

@forkollaider
Copy link

forkollaider commented May 19, 2023

Answer of @frsyuki works well if there are only 2 fields, however if there more ,then this solution will be overcomplicated.
I found useful idea on stack overflow. So what is the solution: to use refine and Object iterators

const atLeastOneDefined = (obj: Record<string | number | symbol, unknown>) =>
  Object.values(obj).some(v => v !== undefined);
  
  const schema = z.object(someSchema).refine(atLeastOneDefined)

And then it is possible to customise checks for your task

@1mehdifaraji
Copy link

I've handled this situation with if and else like this :

export const validateGetOrderQuery = (({ query }, res, next) => {
  const { success, errors } = validateGetOrderId(query);

  const { success: userIdSuccess, errors: userIdErrors } =
    validateGetOrderUserId(query);

  if (success) {
    next();
  } else if (userIdSuccess) {
    next();
  } else if (!success) {
    const err: Error = new Error(JSON.stringify(errors));
    err.statusCode = 401;
    next(err);
  } else {
    const err: Error = new Error(JSON.stringify(userIdErrors));
    err.statusCode = 401;
    next(err);
  }
}) as RequestHandler;

This way the request should have id or userId query parameter otherwise it will throw error as response .

@exoego
Copy link

exoego commented Sep 30, 2023

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"})}));

This is type-safe, but the validation error message is not user-friendly if a client is not TypeScript.
If both fields are missing,

{
  "email":["Required"],
  "username":["Required"],
}

And if both fields are present,

{
  "email": ["Expected undefined, received string"],
  "username": ["Expected undefined, received string"],
}

It appears that errorMap is not used as expected.

@exoego
Copy link

exoego commented Oct 1, 2023

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.

@kwameglide
Copy link

Doesn't the use of partial() make all fields optional, essentially nullifying each field's validators...??

@bennycode
Copy link

Using refine or or on my Zod schema turns my schema from a z.ZodObject into a z.ZodUnion. Thus I am getting problems in my code (TS2416) that some parts are not assignable to the same property in base type (I am using implicit return types in most of my functions). Is there a workaround to fix this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests