You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Since the introduction, the discriminatedUnion (further called union) its very awkward to use and usually involves a lot of dumb mapping-code where none should be needed in order to satisfy the type-checking and not bypass it with type asserts (foo as xyz). And also frequently the need to simply work around the union and simply use and map the types directly.
treeifyError
I use safeParse with once format, now treeifyError to propagate type-safe errors to the UI. It removes a lot of blind-spots, compared to just using unstructured generic error result types. The issue is that the result type on treeifyError is useless on a parse result from a union, because the error types are not discriminated. This example illustrates what I mean: TS Playground
In order to make the error result usable, I have to:
Pre-parse the unknown input in order to extract just the type.
Use the type pre-parsed from the input to branch (if-else or switch) through possible types
Bypass union, instead use the types that are contained within the union based on the previously pre-parsed and matched type
Call on the specific type from the union to safeParse and then result something like {type, error: z.treeifyError(parse.error)}
As you can see, for this instance I had basically to entirely work around the union.
Access individual types via discriminant value (enum-like)
We can access the union types as a typed tuple via union.options. But there is no way to access it by the discriminant value example
The union has just one angle to be access from, everything else becomes messy. Often times you need map some or all or none and just get the errors nicely.
You're right—Zod 4's discriminatedUnion still requires a fair bit of manual mapping for ergonomic type-safe handling, especially around error reporting and accessing schemas by discriminant value. The .options property exposes the tuple of schemas, but there's no public API to map from a discriminant value directly to its schema. Internally, Zod maintains a map for this, but it's not exposed for general use (source).
For error handling, treeifyError and format don't discriminate errors by union member—they aggregate errors, so you can't easily branch on the discriminant in your UI (source). A recent update (Zod 4.0.10) does add a discriminator property to invalid_union errors, which helps a bit with debugging (source), but it doesn't solve the ergonomic mapping problem.
There are some community workarounds:
Use a utility like orFallback to handle unknown discriminants and reduce boilerplate (discussion).
Use preprocess to map unknown discriminants to a fallback type (discussion).
For type narrowing after parsing, use TypeScript's Extract utility (example).
For introspection, you can access the internal _zod.propValues map on a discriminated union instance to see all possible discriminant values, but this is not a documented public API and may change (test example).
There's ongoing discussion about making error formatting and union member access more ergonomic (issue), but as of now, the best approach is still to preprocess or branch manually, or use the internal maps if you're comfortable with the risk.
If these workarounds help or you have further questions, feel free to close the issue or ask for more details!
It'd also be nice if the $ZodIssueInvalidUnion error exposed a list of valid variants along the discriminator property. In case of an invalid discriminator value, I'd like to return an error message that enumerates allowed values. This can be done by using internal APIs, but it'd be preferable not to have to rely on internals.
import{z}from'zod';consterrorMap: z.core.$ZodErrorMap=(issue)=>{switch(issue.code){case'invalid_union': {const{ discriminator }=issue;if(discriminator!==undefined){constschema=issue.instasz.ZodDiscriminatedUnion<readonlyz.ZodObject[]>;// It'd be nice if the following variable was an issue's property.constvariants=schema.options.map(option=>option.shape[discriminator]._zod.values!).reduce<z.util.PrimitiveSet>((acc,curr)=>acc.union(curr),newSet());constvariantsText=Array.from(variants,variant=>JSON.stringify(variant)).join(', ');return`expected one of: ${variantsText}`;}returnundefined;}default:
returnundefined;}};constschema=z.discriminatedUnion('test',[z.object({test: z.null()}),z.object({test: z.literal('foo')}),z.object({test: z.literal('bar')}),],{error: errorMap});constresult=schema.safeParse({test: ''});console.log(result.error?.issues);// [// {// code: 'invalid_union',// errors: [],// note: 'No matching discriminator',// discriminator: 'test',// path: [ 'test' ],// message: 'expected one of: null, "foo", "bar"'// }// ]
Hi — opened #5947 which addresses the second point of the original issue ("Access individual types via discriminant value").
It exposes two new public members on ZodDiscriminatedUnion:
schema.get(value) — returns the option schema matching the literal discriminator value, narrowed at the type level (handles members declared via z.literal([...]) with multiple values).
schema.optionsMap — a ReadonlyMap<DiscriminatorValue, OptionSchema> for iteration / introspection.
Note that the first point (treeifyError losing per-member discrimination) is partly mitigated by this in user-land:
A first-class helper (e.g. treeifyDiscriminatedUnionError(schema, error) returning a discriminated tree per member) would still be valuable to avoid the double-parse and to remove the manual narrowing — happy to follow up with that as a separate PR if there's interest, otherwise this issue could be reduced in scope to just the treeifyError helper.
Also worth noting: the related #5670 (closed by #5723, in v4.4.0+) already added options: Primitive[] to the invalid_union issue when the discriminator doesn't match, so the related request from @gebsh upthread is already covered via issue.options.
This is something I'm doing right now and was mentioned in my initial post:
Your PR would improve the situation with this approach indeed. The above mentioned approach right now requires me to directly import the disc-union types around the disc-union. e. G. import {TypeAFromUnion, TypeBFromUnion}
Changes on the disc-union are not tracked this way and code might break or handle the cases in a non-exhaustive way, requiring further out of the disc-union mapping to ensure exhaustiveness.
Your PR would remove the need to import and pick the individual types around the disc-enum and simplify, when needed, exhaustiveness.
z.getDiscriminatedOption() shipped in 4.5.0 (#5947). It takes a discriminator value and hands back the matching member, which covers the access-by-discriminant half of this.
The error-tree typing is the general unsoundness of typing errors by output rather than anything specific to discriminated unions — z.treeifyError<unknown>(errors) opts out of that assumption. Closing this out.
Activity
dosubot commentedon Aug 11, 2025
You're right—Zod 4's
discriminatedUnionstill requires a fair bit of manual mapping for ergonomic type-safe handling, especially around error reporting and accessing schemas by discriminant value. The.optionsproperty exposes the tuple of schemas, but there's no public API to map from a discriminant value directly to its schema. Internally, Zod maintains a map for this, but it's not exposed for general use (source).For error handling,
treeifyErrorandformatdon't discriminate errors by union member—they aggregate errors, so you can't easily branch on the discriminant in your UI (source). A recent update (Zod 4.0.10) does add adiscriminatorproperty toinvalid_unionerrors, which helps a bit with debugging (source), but it doesn't solve the ergonomic mapping problem.There are some community workarounds:
orFallbackto handle unknown discriminants and reduce boilerplate (discussion).preprocessto map unknown discriminants to a fallback type (discussion).Extractutility (example)._zod.propValuesmap on a discriminated union instance to see all possible discriminant values, but this is not a documented public API and may change (test example).There's ongoing discussion about making error formatting and union member access more ergonomic (issue), but as of now, the best approach is still to preprocess or branch manually, or use the internal maps if you're comfortable with the risk.
If these workarounds help or you have further questions, feel free to close the issue or ask for more details!
To reply, just mention @dosu.
Docs are dead. Just use Dosu.
gebsh commentedon Oct 20, 2025
It'd also be nice if the
$ZodIssueInvalidUnionerror exposed a list of valid variants along thediscriminatorproperty. In case of an invalid discriminator value, I'd like to return an error message that enumerates allowed values. This can be done by using internal APIs, but it'd be preferable not to have to rely on internals.dosubot commentedon Jan 24, 2026
Hi, @akomm. I'm Dosu, and I'm helping the Zod team manage their backlog and am marking this issue as stale.
Issue Summary:
discriminatedUnionusage, specifically the need for manual mapping or type assertions.treeifyErrordoes not properly discriminate union errors for clearer error reporting.discriminatorproperty oninvalid_unionerrors, but ergonomic mapping and error discrimination remain limited.invalid_unionerrors to expose valid discriminator variants to improve error clarity.Next Steps:
Thanks for your understanding and contribution!
akomm commentedon Jan 26, 2026
Still relevant.
dokson commentedon May 4, 2026
Hi — opened #5947 which addresses the second point of the original issue ("Access individual types via discriminant value").
It exposes two new public members on
ZodDiscriminatedUnion:schema.get(value)— returns the option schema matching the literal discriminator value, narrowed at the type level (handles members declared viaz.literal([...])with multiple values).schema.optionsMap— aReadonlyMap<DiscriminatorValue, OptionSchema>for iteration / introspection.Note that the first point (
treeifyErrorlosing per-member discrimination) is partly mitigated by this in user-land:A first-class helper (e.g.
treeifyDiscriminatedUnionError(schema, error)returning a discriminated tree per member) would still be valuable to avoid the double-parse and to remove the manual narrowing — happy to follow up with that as a separate PR if there's interest, otherwise this issue could be reduced in scope to just thetreeifyErrorhelper.Also worth noting: the related #5670 (closed by #5723, in v4.4.0+) already added
options: Primitive[]to theinvalid_unionissue when the discriminator doesn't match, so the related request from @gebsh upthread is already covered viaissue.options.feat(v4): expose optionsMap and get(value) on ZodDiscriminatedUnion
akomm commentedon May 7, 2026
Yes.

This is something I'm doing right now and was mentioned in my initial post:
Your PR would improve the situation with this approach indeed. The above mentioned approach right now requires me to directly import the disc-union types around the disc-union. e. G.
import {TypeAFromUnion, TypeBFromUnion}Changes on the disc-union are not tracked this way and code might break or handle the cases in a non-exhaustive way, requiring further out of the disc-union mapping to ensure exhaustiveness.
Your PR would remove the need to import and pick the individual types around the disc-enum and simplify, when needed, exhaustiveness.
feat(v4): add z.getDiscriminatedOption (#5947)
colinhacks commentedon Sep 1, 2026
z.getDiscriminatedOption()shipped in 4.5.0 (#5947). It takes a discriminator value and hands back the matching member, which covers the access-by-discriminant half of this.The error-tree typing is the general unsoundness of typing errors by output rather than anything specific to discriminated unions —
z.treeifyError<unknown>(errors)opts out of that assumption. Closing this out.