I'm working with a validation schema using Zod, where I have an items field that represents an array of objects. In this scenario, I'm specifically using it for invoice items. Each item is an object with various properties.
I'm facing a challenge in defining a validation schema that enforces the following condition: If the id property within an item is set, then all other fields within that item should be optional (indicating that the item is being updated). Conversely, if the id property is not set, then all other fields should be required (indicating that a new item is being created).
How can I achieve this conditional validation in Zod?
items: z.array(z.object({
id: z.number({required_error: "'id' is required", invalid_type_error: "'id' must be a number"}).positive("'id' must be a positive number").optional(),
articleId: z.number({required_error: "'articleId' is required", invalid_type_error: "'articleId' must be a number"}).positive("'articleId' must be a positive number"),
sku: z.number({required_error: "'sku' is required", invalid_type_error: "'sku' must be a number"}).positive("'sku' must be a positiv number"),
description: z.string({ required_error: "'description' is required" }).nonempty("'description' cannot be empty").max(1000, { message: "'description' is too long" }),
quantity: z.number({required_error: "'quantity' is required", invalid_type_error: "'quantity' must be a number"}).positive("'quantity' must be a positive number"),
price: z.number({required_error: "'price' is required", invalid_type_error: "'price' must be a number"}).positive("'price' must be a positive number"),
term: z.enum(["month", "year", "flat", "piece"], { required_error: "'term' is required" }),
})),
Have you tried z.or()?
const schema = z.object({
id: z.string(),
}).or(z.object({
name: z.string(),
whatever: z.number(),
etc: z.string()
}))
type T = z.infer<typeof schema>
/*
T: {id: string} | {name: string; whatever: number; etc: string}
*/
You can try using superRefine method to add conditional validation to your previous schema like this:
const itemSchema = z
.object({
id: z
.number({
required_error: "'id' is required",
invalid_type_error: "'id' must be a number",
})
.positive("'id' must be a positive number")
.optional(),
articleId: z
.number({
required_error: "'articleId' is required",
invalid_type_error: "'articleId' must be a number",
})
.positive("'articleId' must be a positive number"),
sku: z
.number({
required_error: "'sku' is required",
invalid_type_error: "'sku' must be a number",
})
.positive("'sku' must be a positive number"),
description: z
.string({ required_error: "'description' is required" })
.nonempty("'description' cannot be empty")
.max(1000, { message: "'description' is too long" }),
quantity: z
.number({
required_error: "'quantity' is required",
invalid_type_error: "'quantity' must be a number",
})
.positive("'quantity' must be a positive number"),
price: z
.number({
required_error: "'price' is required",
invalid_type_error: "'price' must be a number",
})
.positive("'price' must be a positive number"),
term: z.enum(["month", "year", "flat", "piece"], {
required_error: "'term' is required",
}),
})
.superRefine(
(
{ id, articleId, sku, description, quantity, price, term },
refinementContext
) => {
if (id !== undefined) {
// If id is defined, make all other fields optional
return;
} else {
// If id is not defined, make all other fields required
const fieldsToCheck = [
"articleId",
"sku",
"description",
"quantity",
"price",
"term",
];
for (const field of fieldsToCheck) {
if (eval(field) === undefined) {
refinementContext.addIssue({
code: z.ZodIssueCode.custom,
message: `${field} is required when 'id' is not set`,
path: [field],
});
}
}
}
}
);
const items = z.array(itemSchema);
const validItem = {
articleId: 123,
sku: 456,
description: "Product ABC",
quantity: 5,
price: 10.99,
term: "month",
};
const isValid = items.safeParse([validItem]);
console.log(isValid.success); // Should output true
const invalidItem = {
articleId: 123,
sku: 456,
//description is missing
quantity: 5,
price: 10.99,
term: "month",
};
const isInvalid = items.safeParse([invalidItem]);
console.log(isInvalid.success); // Should output false
This code was adapted from this repository on GitHub, which contained an implementation similar to yours: https://github.com/colinhacks/zod/discussions/938#discussioncomment-6952970
I hope this solves your problem.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With