Closed Invertisment closed 4 years ago
Simple answer:
You have access to all this error information already. Zod throws a special subclass of Error called ZodError
that contains detailed error information in the .errors
property:
import * as z from "zod";
try {
z.union([z.object({ a: z.number() }), z.object({ b: z.number() })]).parse({ t: 1 });
} catch (err) {
if (err instanceof z.ZodError) {
console.log(JSON.stringify(err.errors, null, 2));
}
}
This wil print:
[
{
"code": "invalid_union",
"unionErrors": [
{
"errors": [
{
"code": "unrecognized_keys",
"keys": [
"t"
],
"path": [],
"message": "Unrecognized key(s) in object: 't'"
},
{
"code": "invalid_type",
"expected": "number",
"received": "undefined",
"path": [
"a"
],
"message": "Required"
}
]
},
{
"errors": [
{
"code": "unrecognized_keys",
"keys": [
"t"
],
"path": [],
"message": "Unrecognized key(s) in object: 't'"
},
{
"code": "invalid_type",
"expected": "number",
"received": "undefined",
"path": [
"b"
],
"message": "Required"
}
]
}
],
"path": [],
"message": "Invalid input"
}
]
More complicated answer:
Error handling in Zod is complex enough that I split it into it's own README: https://github.com/vriad/zod/blob/master/ERROR_HANDLING.md
That guide explains how to do this. Getting the exact error information you need can feel a bit like spelunking, but I'm pretty sure this complexity is irreducible. Believe me, I've tried :)
Here's how to do what you want:
import * as z from '.';
try {
z.union([z.string(), z.number()]).parse(true);
} catch (err) {
// check for ZodError
if (err instanceof z.ZodError) {
// loop over suberrors
for (const suberr of err.errors) {
// check if suberror is an `invalid_union` error
// inside the for loop, `suberr` will have additional
// properties specific to union errors (e.g. `unionErrors`)
if (suberr.code === z.ZodErrorCode.invalid_union) {
// suberr.unionErrors is an array of ZodErrors for each union component
suberr.unionErrors; // ZodError[]
console.log(suberr.unionErrors.map(e => e.message).join("\n"))
}
}
}
}
If you don't want to write this error handling code every time you want to handle union errors you should write your own Error Map which lets you customize the message for every kind of error in Zod. You then pass your error map as a parameter to .parse(). Instructions for doing so are all explained in the guide. 👍
You can get a sense for all the error data available to you like this:
It seems like this could be improved with a union resolver function as I proposed in https://github.com/vriad/zod/issues/100#issuecomment-667584554. As long as the input matches well enough (e.g. with a discriminator field) to be recognized as an element of the union in userland, the errors could be simplified to a single case instead of needing to resolve unionErrors
within an invalid_union
error type.
enum Number {
FLOAT,
INT,
}
const floatObj = z.object({
kind: z.literal(Number.FLOAT),
value: z.number(),
});
const intObj = z.object({
kind: z.literal(Number.INT),
value: z.number().refine(n => n % 1 === 0, "not_int"),
});
const resolver = (val: unknown) =>
val && typeof val === 'object'
? val.kind === Number.FLOAT
? floatObj
: intObj
: null;
const union = z.union([floatObj, intObj], resolver);
const input = {
kind: Number.INT,
value: 5.5,
};
union.parse(input); // same result as intObj.parse(input);
So here, parsing input
with into either the union
or intObj
schemas should result in:
Error: 1 validation issue(s)
Issue #0: custom_error at value
not_int
Thanks, I've found all the error info. It's good that it's there.
In my case there was no discriminator field and I decided to print out my errors in a nested way:
I'm not sure what you mean by "irreducible".
In my case I used recursive function with a very similar suberr.code === z.ZodErrorCode.invalid_union
check.
And what I did is simply concatenated all errors and checked them recursively. I also added space increases so that deeply nested errors would have some spacing.
This way I'll have them printed even for deeply recursive types.
I think that it's entirely possible to have these messages translated to my initially desired TS-like form.
To do this it's needed to have a string representation of the type and to print it into the error object's message/field OR add the type itself to use .toString()
on it.
This way it could be printed later.
Most people want the default error messages to be something that can be presented to an end-user (say, as an error message on a form input) so I'm not willing to change the default union message to something like '{ t: 1 }' is not assignable to parameter of type '{ a: number } | { b: number }'
.
There's no way for me to stringify the full error payload in a way that makes everyone happy so I'm not going to try. Developers should decide for themselves how to generate error messages from the errors
object. 👍
convenient syntax for random travelers:
z.union(
[
z.object({ postId: z.number(), parentCommentId: z.undefined() }),
z.object({ postId: z.undefined(), parentCommentId: z.number() }),
],
{ errorMap: () => ({ message: "Either postId or parentCommentId is required." }) }
)
I still feel like there should be a way to control the invalid union message by default. Thanks to @TamirCode for a workable current solution. Saved my day
I guess the ❤️ count on @TamirCode's suggestion indicate a possible feature request 😄
I'd like to have more information or even TS-like details for union errors (Add a flag to parse?):
My types:
So what I currently get from this is:
What I'd like to get is something more similar to this:
Is this sensible|doable|easy|hard?