zod icon indicating copy to clipboard operation
zod copied to clipboard

discriminated union cant be nested

Open Fuzzyma opened this issue 2 years ago • 5 comments

Currently it is impossible to allow discriminated unions as argument to another discriminated union. Typescript however allows that so it should be allowed in zod as well

Example Typescript and zod: https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgLzgXzgMyhEcDkyEAJvgLABQlMAnmAKZwCCcAvIpXHMcAM4DGUUMAB2AQxjQAjAC44MKAFd6Abk5wQvAOZzeC0VrUU0lanUYAhFuwTqwYqGJCy4IxSABG9KEa48BQiCiEtAATHKYYgA2vKrq-oLC4pJQLpEx9JQmVBS0DHAWFmwcFH58iUHJYXIKyr5w9o4g4a7uXj7x5YHBKWnRsVmmueYFxQAUVnAAPgUWAJRDeYwAwsUsMxZDlPwQInpYIuNgcstzbAB8JerAmHBjAIRgAHQJ3VWpZ7alXHA3d48vLpJEJQUKfdQ-OA7PYQKL0J5RCBaMbPRpOKQLb5cDD0DIlSFcaG8WHwxHI1EOJxgiHodQ4vFfSFEkkIpEop6aLSY7GDHJE+AAfWsKCeEA8ACt6PwYGNGXAoPRJdL6MQ5MgEcAYN5omNavQ5gAadSctVPPRCETIzFoTHbXb7AWTdjqsVKmVynj0ZaKGDLCB6XimtyebxjQ3qBWKWKqkVRTXaqJjdKxcPfBVulWmuNaxyJ5P6rK2ij8uCOorO0USqXu9QAdwAFhIg21Q6muJHo1n47ndUp9Ua04rq5nY92dfnrUWS47iurXsCtcQAKoiYC7Mb4Dsq-AGuAAbUdTF3ZYAukWAPTnuDeHBQXh2vaC1YV+eVCQqldrkQb9PD0i7g8j1LCwzyGS9fhETBvGAuBa2gABre9hnyAAtFDZyeUQoKgAAeJYIFuR1zkoIA

Here is the code for reference:

import { z } from 'zod'

type A = {
  discriminator1: true;
  msg: string;
}

type BA = {
  param1: number;
  discriminator2: false;
  discriminator1: false
}

type BB = {
  discriminator2: true;
  param2: number;
  discriminator1: false
}

type B = (BA | BB)

type C = A | B


const fn = (p: C) => {

  if (!p.discriminator1) {
    if (!p.discriminator2) {
      console.log(p.param1)
    } else {
      console.log(p.param2)
    }
  } else {
    console.log(p.msg)
  }
}

const _A = z.object({
  rejected: z.literal(true),
  msg: z.string()
})

const _BA = z.object({
  dieCutCosts: z.number(),
  reused: z.literal(false),
  rejected: z.literal(false)
})

const _BB = z.object({
  what: z.number(),
  reused: z.literal(true),
  rejected: z.literal(false)
})

const _B = z.discriminatedUnion('reused', [_BA, _BB])

// errors
const _C = z.discriminatedUnion('rejected', [_A, _B])

// infer _B works
type ZZ = z.infer<typeof _B>

Fuzzyma avatar Jan 13 '23 21:01 Fuzzyma

+1

yigithanyucedag avatar Dec 12 '23 13:12 yigithanyucedag

Any updates on this issue?

danyroza avatar Dec 30 '23 12:12 danyroza

Bumping this. Any updates?

justinbaltazar avatar Jan 24 '24 16:01 justinbaltazar

Bumping

is-dev-98 avatar Oct 29 '24 12:10 is-dev-98

Hi all, this is addressed in Zod v4 feel free to have a look here: https://v4.zod.dev/, if this answers your question, please could you close the issue?

Christopher96u avatar Apr 12 '25 00:04 Christopher96u

Zod 4 does support nested discriminated unions, but there are some important requirements and recent changes to be aware of. The key is that the discriminator key (like "rejected" or "reused") must be present on the final object value for each variant, even when nesting. Recent updates have added tests and improved support for these patterns, so if you're on Zod 4, this should work as long as your schemas are structured so that the discriminator is always available at the top level of each variant [PR #3956] [nested union tests].

If you see an error when nesting discriminated unions, it's usually because one of the variants doesn't have the required discriminator key at the top level, or because an older version of Zod is being used. Make sure you're on the latest Zod 4 release.

If you want to merge options from multiple discriminated unions (with the same discriminator key), you can use the .options property to combine them into a new discriminated union, as shown in the v3 docs [v3 docs example].

If your question is answered, please close the issue! If you still see errors with the latest Zod, please share a minimal reproduction (including your Zod version and the exact error message) so @colinhacks can take a closer look.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Join Discord Share on X

dosubot[bot] avatar Jul 22 '25 03:07 dosubot[bot]