Discriminated union default value on the discriminator
Discriminated union default value on the discriminator
Solution:Jump to solution
```ts
const BaseReservation = z.object({
reservationId: z.number(),
reservedBy: z.string(),
});...
39 Replies
I'd like to set a default value for the discriminator of a discriminated union. is it possible and how?
meaning you want to allow it to be either
undefined
or some literal value coming in, and set it to the literal value by default in the undefined
case?TS Playground - An online editor for exploring TypeScript and JavaS...
The Playground lets you write TypeScript or JavaScript online in a safe and sharable way.
In TS terms i was thinking like:
then when the input object to be parsed is
Zod's output would be
āļø that? yeah, you can do that roughly the same way in Zod:
just be careful about this: the input data is dangerously compatible--without the discriminant, the without room data is assignable to the with room data. Good thing you're parsing it and adding a discriminant š
Nope, I don't think that's it.
My input JSON might not have the haveRoom field at all, in which case Zod should use the default false value for it.
With your schema if I pass as obj to parse:
it throws an invalid discriminator value for haveRoom, it should default to false
oh, right, sorry I forgot about that part, but it's the same as the suggestion from earlier. lemme edit.
Nope, same error as before: Invalid discriminator value. Expected true | false
ah, yeah, this seems like a bug. You can parse either schema just fine, but
discriminatedUnion
complains.
(you can use union
instead, which seems to work fine)
yeah, looking at the implementation, we do not actually run the schemas so we don't get the default
transform, we just look up the input data at that key, and for this case, it's undefined.
Unfortunately (or fortunately depending on the way you look at it), discriminatedUnion
is being deprecated in favor of just a smarter union
, so I doubt Colin will want to spend much effort fixing this.
There's a (very) hacky fix that involves parsing with a union and then piping that to the discriminated union which gives you the "best" of both worlds, but mitigates the poor error reporting performance of union
:
š
This is worse performance-wise but better for error reporting, so I wouldn't do this for like an unbounded array of these objects (in that case, I might consider a custom transform)
oh also, drop the z.never()
that was unnecessary and actually doesn't work because undefined
is not never
š
thanks for all the info, I might run with the "hacky" solution until v4 comes out š
If i was to only use union like you suggested here, how would i do it?
just tried, this still doesn't work š¢
I expected this to work, but it doesn't:
Yeah, that's not going to work because the no room case matches both
Solution
Note that the order here is very important because the without room case matches either, but it'll return the first one that matches, so if you have withoutroom first, even the withroom data will match.
that's it! Thank you very much! You've helped me immensely
This will work nice until v4 comes out š
Amazing. If you want the better error reporting just pipe that into a redundant discriminated union again š
would this work for more with/without cases? i can just extend BaseSchema more times and then union all of those?
that's right! Just watch out for assignability and ordering if the objects are very similar to each other.
This worked at last:
@Scott Trinh sorry to bother you again, how would i pipe this to the discriminated union(s?) to get the better error reporting? Thanks
Thanks, i get this at runtime though:
error: A discriminator value for key
haveA
could not be extracted from all schema optionsoh, true, I guess you'd need to redefine the schemas to have a more strict version that doesn't need to handle the no discriminant case.
Can you provide an exact reproduction here, maybe as a TypeScript Playground?
TS Playground - An online editor for exploring TypeScript and JavaS...
The Playground lets you write TypeScript or JavaScript online in a safe and sharable way.
sorry for the huge link, I don't know if discord supports markdown
ha, no problem. it does support markdown links like github (e.g.
[description](href)
)
OK, the issue here is that your discriminants are not defined as literals, but rather as coerced booleans. This works:
If you're trying to handle possible string-y discriminant here, you'll need to probably use pipe here, but I don't think you need to worry about that unless you're parsing form encoded data or URL search parameters.
Also, not everything needs to extend Base
, so here's a working (I think?) example:
TS Playground - An online editor for exploring TypeScript and JavaS...
The Playground lets you write TypeScript or JavaScript online in a safe and sharable way.
thank you very much for support. the problem with using
z.literal()
instead of z.coerce.boolean()
is that the haveX properties come from parsing FormData checkboxes, and they come with the default "on" value... so maybe it's better to z.literal("on").pipe(z.literal(true))
to transform them?pipe doesn't work like that, it would try to check to see if the literal value
"on"
was the literal value true
Is the input data exactly: "on" | "off"
or are there other cases like undefined
?
I think it would be helpful to share some valid input data because this whole time I was thinking you were dealing with these keys being missing but if they're there, that's a completely different thing š
in short the input comes either from a form or from a sqlite db.
haveA, haveB, haveC are checkboxes so as per HTML standard if they're not checked they are not sent on form submit; if they're checked the key/value pair is sent and the default value is the string "on" if not otherwise specified. I'll send some input examples as soon as i get back to my computer
ahh, and you're trying to use the same schema to process data from sqlite (which uses a
boolean
here?) and the form?sqlite does not have boolean, uses 0 or 1 integer. so z.coerce.boolean() was working fine.
so all in all
haveX: "on" | 0 | 1 | undefined
so zod should parse true if value is "on" or 1 , false if 0 or undefined
I'm assuming typescript's undefined means the property might be missing on the input data so the equivalent of zod's .optional()
yeah, that's roughly correct (although strict property access means the semantics here are a little bit off until we fix that in zod 4)
Ok, that's helpful. I'd use this:
And then use those for the schemas like:
well, I guess this is a little more complicated than that because
discriminatedUnion
doesn't like a union discriminant even though the output is a constant literal.
I think there are two general issues:
1. discriminatedUnion
is broken for anything but the most trivial case.
2. Trying to write a schema that work across multiple boundary points is tough.
The first point is being addressed in an upcoming version of Zod.
The second point is a little more subtle. I really want to write a blog post about this, but in essence, I think it's usually prudent to write 1+n
schemas where n
is the number of different I/O boundaries you might have, so in your case: 3.
1. Domain object (the schema for the object as you use it in your actual code)
2. Encoding schema for form data
3. Encoding/decoding schema for sqlite data
You can skip the Domain Object one usually, but I find it a nice thing to have within your actual code that's the real type, and you can use it to pipe the other schemas into it to ensure they're fully 100% compliant. I'll take a swing at doing this for your example schema and see if it works well.TS Playground - An online editor for exploring TypeScript and JavaS...
The Playground lets you write TypeScript or JavaScript online in a safe and sharable way.
Note that I've made type aliases for the input type of each boundary schema so that you have a type for what the input data actually looks like that you can use for tests, etc.
i've left out the decoding back to Sqlite transformation (
true
-> 1
, etc) as that is (hopefully!) being done by the database driver already, but feel free to make yourself a schema that is an inverse there.That's wild, thank you! I'll try to integrate this huge help into my code. I would gladly read your blog post about this š
It seems to work for my initial testing... But I'm having some trouble with the UI: my form page backend uses
safeParse()
and if !result.success
returns the form page pre-populated with result.data
which is... inconsistent, because i can check the checkbox for haveRoom
and not choose any valid option from the <select>
associated to roomNumber
, in which case the backend receives an incosistent object with any of the schemas defined. Is it a common problem? How is it usually solved? Thanks again for the incredible patience and help you've given meBut I'm having some trouble with the UI: my form page backend uses safeParse() and if !result.success returns the form page pre-populated with result.data which is... inconsistentI'm not sure I follow this.
result.data
should be empty if result.success === false
Ah, I'm sorry, I'm using the validator middleware of Hono web framework. It uses .spa() under the hood and i think returns the input as result.data even if result.success === false. I guess I'm on my own for this one, as zod does not return a "partial" parsed object
yeah š
I feel like that's a bad idea, but some people have actually asked for this behavior (partial parsing)
fixed with god's intended language: HTML. multiple options can be selected but only the last one will be truly selected (its text will appear in the UI and its value will be sent)
so i keep the default invalid option "please choose ..." always selected and then if there is a previous valid selection it will automatically override, otherwise it will fallback to the default disabled option