Relationship between schema object fields
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!
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.
Oh c'mon...
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() },
])
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" })) },
])
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
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...
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.
Don't. You. Dare.
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.
Nope.
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=""
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.
Nope, but I'm curious about the new possible replacement switch, so let's wait!
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.
Still waiting
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.
Naaaaaa-ah.