zod icon indicating copy to clipboard operation
zod copied to clipboard

Relationship between schema object fields

Open alexandercerutti opened this issue 3 years ago • 11 comments

Hello there! I was exploring Zod with the intent of migrating from Joi. This looks awesome and makes me feel the schemas are more solid. I think it might be an excellent ally for my open-source library!

I observed how Joi owns a way to specify a relationship between schema object fields through the properties when (doc) and with (doc).

So, I could write:

Joi.object().keys({
	 ...,        
	 value: Joi
		.alternatives(z.string().allow(""), z.number(), z.date().iso())
		.required(),
	numberStyle: Joi
		.string()
		.regex(
			/(aaaaa|bbbbbb|ccccccc|ddddddd)/,
		)
		.when("value", {
			is: Joi.number(),
			otherwise: Joi.string().forbidden(),
		}),
});

Which tells Joi to forbid numberStyle if value is a not a number, or

Joi.object().keys({ ... }).with("webServiceURL", "authenticationToken");

Which enforces that two properties must appear at the same time for the schema to be valid.

So, is there a way to define a relationship between keys, if necessary with a condition? I guess something could be done with z.never() but I can't figure it out right now.

A solution that comes to my mind might be to create a discriminated union of objects based on value for what concerns when.

I would do something like this:

z.object<Field>({
	/** The rest of props... */
}).merge(
	z.discriminatedUnion("value", [
		z.object<Field>({
			value: z.string().or(isoDateString),
			numberStyle: z.never(),
		}),
		z.object({
			value: z.literal(z.number()),
			numberStyle: z
				.string()
				.regex(
					/(aaaaa|bbbbbb|ccccccc|dddddddd)/,
				)
				.optional(),
		}),
	]),
);

But, it seems that value must be literal, but in my case, it is a generic string or a generic number. So how can I achieve this? Would union still be okay?

For what concerns what, instead, I don't yet know how to create the relationship.

I guess the big issue here is that I'm attempting to migrate my Joi schemas 1-to-1 to Zod, but it probably requires a whole mindset change. 😄

Thank you very much!

alexandercerutti avatar May 15 '22 00:05 alexandercerutti

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.

stale[bot] avatar Jul 14 '22 00:07 stale[bot]

Oh c'mon...

alexandercerutti avatar Jul 14 '22 06:07 alexandercerutti

Maybe something like this?

z.union([
  z.object({ value: z.string(), numberStyle: z.never() }),
  z.object({ value: z.number(), numberStyle: z.string().regex(abcdRegexp).optional() },
])

scotttrinh avatar Jul 22 '22 18:07 scotttrinh

Basically you're making a true union not a discriminated union. If you want to make this easier to narrow on the typescript side you could transform and add a discriminant, like:

z.union([
  z.object({ value: z.string(), numberStyle: z.never() }).transform(v => ({ ...v, type: "STRING" })),
  z.object({ value: z.number(), numberStyle: z.string().regex(abcdRegexp).optional().transform(v => ({ ...v, type: "NUMBER" })) },
])

scotttrinh avatar Jul 22 '22 18:07 scotttrinh

Hey @scotttrinh, thanks for your reply. I have to try the solution you proposed, but it is not very clear to me the transform with "STRING" | "NUMBER". Does it have any special meaning?

Thank you

alexandercerutti avatar Jul 22 '22 21:07 alexandercerutti

I've tried to apply what you said and it apparently seems to work (I have to thoroughly test it because of what follows).

I've done this:

export const OverridableProps = z.object({
		/** props **/
})
	.and(
		z.union([
			z.object({
				webService: z.never(),
				authenticationToken: z.never(),
			}),
			z.object({
				webService: z.string(),
				authenticationToken: z.string(),
			}),
		]),
	);

This seems to return a ZodIntersection.

So I'm not able to merge it with other schemas to create a single object. Also, I'm not able to merge the union above with the object above instead of using and.

export const AllProps = z
	.object({})
	.merge(KindsProps)
	.merge(PropsFromMethods)
	.merge(OverridableProps);

(I used z.object() just as a test, but it is the same if I take, for example, KindsProps and use merge on it).

Seems like .merge does not support intersections or unions, so I'm stuck here...

alexandercerutti avatar Jul 24 '22 10:07 alexandercerutti

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.

stale[bot] avatar Sep 22 '22 13:09 stale[bot]

Don't. You. Dare.

alexandercerutti avatar Sep 22 '22 13:09 alexandercerutti

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.

stale[bot] avatar Nov 21 '22 19:11 stale[bot]

Nope.

alexandercerutti avatar Nov 21 '22 20:11 alexandercerutti

Hello! This is something I'm interested too.

In my use-case I would really love to use zod along with NestJS in order to validate microservice required env variables.

For example in of my schemas using joi I have something like this:

SOME_TOKEN: Joi.when('NODE_ENV', {
    is: NodeEnv.Test,
    then: Joi.string().allow('').default(''),
    otherwise: Joi.string().required(),
  }),

For sure I'm still able to do the validation using zod setting SOME_TOKEN as string always required having

# .env
SOME_TOKEN=<real_token>
# .env.test
SOME_TOKEN=""

andreafspeziale avatar Dec 07 '22 15:12 andreafspeziale

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.

stale[bot] avatar Mar 07 '23 20:03 stale[bot]

Nope, but I'm curious about the new possible replacement switch, so let's wait!

alexandercerutti avatar Mar 07 '23 20:03 alexandercerutti

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.

stale[bot] avatar Jun 08 '23 11:06 stale[bot]

Still waiting

alexandercerutti avatar Jun 08 '23 13:06 alexandercerutti

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.

stale[bot] avatar Sep 07 '23 00:09 stale[bot]

Naaaaaa-ah.

alexandercerutti avatar Sep 07 '23 06:09 alexandercerutti