Nested discriminated unions

See original GitHub issue

I have a hierarchy of objects, where the first level uses a type-property to discriminate, and the second level uses a subtype.

An example diagram showing 3 instances on the first level (Foo, Bar & Baz), and then Baz having two subtypes under it (Able & Baker):

    [root]
   /  |   \
Foo  Bar   Baz
          /   \
        Able  Baker

It’s possible to type this in TS with something like

type NestedDu = 
   { type: "foo" }
 | { type: "bar" }
 | { type: "baz", subtype: "able" }
 | { type: "baz", subtype: "baker" };

(Of course there are more fields on each property, but just keeping it minimal for the ease of understanding)

I tried to construct a Zod-schema using the following

const nestedDU = z.discriminatedUnion('type', [
    z.object({
        "type": z.literal("foo")
    }),
    z.object({
        "type": z.literal("bar")
    }),
    z.discriminatedUnion('subtype', [
        z.object({
            "type": z.literal("baz"),
            "subtype": z.literal("able")
        }),
        z.object({
            "type": z.literal("baz"),
            "subtype": z.literal("baker")
        }),
    ]),
]);

But this doesn’t work, since the outer discriminated union doesn’t allow the inner one.

Is there any way to make this work as-is - or would it need additional work on Zod itself to make it work? I can try to create a PR if needed, but maybe it’s already possible and I’m just missing something.

Kind regards Morten

Issue Analytics

  • State:open
  • Created 10 months ago
  • Comments:12 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
fangelcommented, Dec 5, 2022

My half-though with the array of discriminators was that it could potentially be something that could be checked with a recursive type-definition My thinking would be to recursively loop over the discriminators and the properties and match them up - if we exhaust the properties before the discriminators, then type type should resolve to never. I believe this should be achievable with splitting the list up into the head & tail and seeing if the tail is empty.


Another option is to look into getting ZodObject.merge to accept a discriminate union as the input, because then this would be achievable

z.discriminatedUnion('type', [
    z.object({
        "type": z.literal("foo")
    }),
    z.object({
        "type": z.literal("bar")
    }),
    z.object({
        "type": z.literal("baz")
    }).merge(z.discriminatedUnion('subtype', [
        z.object({
            "subtype": z.literal("able")
        }),
        z.object({
            "subtype": z.literal("baker")
        }),
    ])),
]);

It currently fails because ZodDiscriminateUnion isn’t assignable to AnyZodObject. I guess the semantics are a little weird since then .merge would end up basically converting the original object to a union.

So semantically it’s probably nicer to just have DUs nest the way you naively expect them to - i.e. adding the inner DU to the list of objects in the outer DU.

0reactions
fangelcommented, Dec 12, 2022

I ran your PR-branch locally, and can confirm that it worked exactly like I’d hope it would. Wonderful work!

Read more comments on GitHub >

github_iconTop Results From Across the Web

F#: Nested discriminated unions and matching - Stack Overflow
I have 2 nested discriminated unions:
Read more >
ValueError raised when Nested Discriminated Unions are ...
In my case the error appears when I use an Annotated field twice in the same model. Using Pydantic v1.9.0. from typing import...
Read more >
The case for Discriminated Union Types with Typescript
Discriminated Unions combine more than one technique and create self-contained types. Types that carry all the information to use them without ...
Read more >
Discriminated Unions - F# | Microsoft Learn
Discriminated unions provide support for values that can be one of a number of named cases, possibly each with different values and types....
Read more >
Lesson 21. Modeling relationships in F# - Get Programming ...
Listing 21.6. Nested discriminated unions. type MMCDisk = #1 | RsMmc | MmcPlus | SecureMMC type Disk = | MMC of MMCDisk *...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found