Repository: colinhacks/zod
Pull request #4402 (2025-05-18 21:16:45)

Add back discriminator to `z.discriminatedUnion()` API
Unfortunately a last-minute breaking change (sort of) here. TLDR: You need to continue specifying the discriminator key:

```ts
// ✅
z.discriminatedUnion("type", [
  z.object({ type: z.literal("a"), a: z.string() }),
  z.object({ type: z.literal("b"), b: z.string() }),
]);

// ❌
z.discriminatedUnion([
  z.object({ type: z.literal("a"), a: z.string() }),
  z.object({ type: z.literal("b"), b: z.string() }),
]);
```

In earlier betas there was no need to specify the discriminator key. But I'm adding it back now for a couple reasons:

1. The old implementation was getting unnecessarily hairy. There were edge cases I didn't anticipate: https://github.com/colinhacks/zod/issues/4399
2. From an API design perspective, specifying a discriminator is not difficult, and in fact kind of nice. 
3. Bundle size: An explicit key reduces the bundle size needed by about half.
4. Improved error reporting. In the old implementation it was difficult to determine which discriminator key was failing to match against the input in cases where multiple keys could serve as a discriminator. Now, it's always explicit so the `"path"` can be reliably set in the `"invalid_union"` issue
5. Performance: Ultimately discriminated unions are just an optimization, and an explicit discriminator improves performance by about 50% at least, and far more in cases involving nested discriminated unions.


coderabbitai[bot]: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Auto reviews are disabled on base/target branches other than the default branch.
> 
> 
> 
> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

<details open="true">
<summary>🧪 Generate Unit Tests</summary>

- [ ] <!-- {"checkboxId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} -->   Create PR with Unit Tests
- [ ] <!-- {"checkboxId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} -->   Commit Unit Tests in branch `v4-simplify-disc`
- [ ] <!-- {"checkboxId": "07f1e7d6-8a8e-4e23-9900-8731c2c87f58", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} -->   Post Copyable Unit Tests in Comment

</details>

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=colinhacks/zod&utm_content=4402):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai generate unit tests` to generate unit tests for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->

Patch:
diff --git a/packages/bench/discriminated-union.ts b/packages/bench/discriminated-union.ts
index 946a9a5e..17c06492 100644
--- a/packages/bench/discriminated-union.ts
+++ b/packages/bench/discriminated-union.ts
@@ -3,20 +3,21 @@ import { metabench } from "./metabench.js";
 
 
 import * as z3 from "zod/v3";
-import * as z4 from "zod/v4-mini";
+import * as z4 from "zod/v4";
+import * as z4lib from "zodnext/v4";
 
 
 const z3fields ={
   data1: z3.string(),
   data2: z3.string(),
   data3: z3.string(),
-  data4: z3.string(),
-  data5: z3.string(),
-  data6: z3.string(),
-  data7: z3.string(),
-  data8: z3.string(),
-  data9: z3.string(),
-  data10: z3.string(),
+  // data4: z3.string(),
+  // data5: z3.string(),
+  // data6: z3.string(),
+  // data7: z3.string(),
+  // data8: z3.string(),
+  // data9: z3.string(),
+  // data10: z3.string(),
 }
 const z3Union = z3.union([
   z3.object({
@@ -39,32 +40,30 @@ const z3Union = z3.union([
     type: z3.literal("e"),
     ...z3fields
   }),
-
   z3.object({
     type: z3.literal("f"),
     ...z3fields
   }),
-
   z3.object({
     type: z3.literal("g"),
     ...z3fields
   }),
-  
 ]);
 
 const z3DiscUnion = z3.discriminatedUnion("type", z3Union._def.options);
 
-const z4fields = {
+function makeSchema(z: typeof z4){
+   const z4fields = {
   data1: z4.string(),
   data2: z4.string(),
   data3: z4.string(),
-  data4: z4.string(),
-  data5: z4.string(),
-  data6: z4.string(),
-  data7: z4.string(),
-  data8: z4.string(),
-  data9: z4.string(),
-  data10: z4.string(),
+  // data4: z4.string(),
+  // data5: z4.string(),
+  // data6: z4.string(),
+  // data7: z4.string(),
+  // data8: z4.string(),
+  // data9: z4.string(),
+  // data10: z4.string(),
 }
 const z4Union = z4.union([
   z4.object({
@@ -79,43 +78,53 @@ const z4Union = z4.union([
     type: z4.literal("c"),
     ...z4fields
   }),
-
   z4.object({
     type: z4.literal("d"),
     ...z4fields
   }),
-
   z4.object({
     type: z4.literal("e"),
     ...z4fields
   }),
-
   z4.object({
     type: z4.literal("f"),
     ...z4fields
   }),
-
   z4.object({
     type: z4.literal("g"),
     ...z4fields
   }),
 ]);
+return z4Union;
 
+}
 
-const z4DiscUnion = z4.discriminatedUnion(z4Union.def.options);
-
-const DATA = makeData(1000, () => ({ 
-  type: randomPick(["a", "b", "c", "d", "e", "f", "g"]), 
+const z4Union = makeSchema(z4);
+const z4LibUnion  = makeSchema(z4lib as any);
+const z4LibDiscUnion  = z4lib.discriminatedUnion( z4LibUnion._def.options);
+
+const z4DiscUnion = z4.discriminatedUnion("type", z4Union.def.options);
+
+const DATA = makeData(100, () => ({ 
+  type: randomPick([
+    "a", 
+    "b",
+    "c",
+    "d",
+    "e",
+    "f",
+    "g"
+  ]), 
   data1: randomString(10),
   data2: randomString(10),
   data3: randomString(10),
-  data4: randomString(10),
-  data5: randomString(10),
-  data6: randomString(10),
-  data7: randomString(10),
-  data8: randomString(10),
-  data9: randomString(10),
-  data10: randomString(10),
+  // data4: randomString(10),
+  // data5: randomString(10),
+  // data6: randomString(10),
+  // data7: randomString(10),
+  // data8: randomString(10),
+  // data9: randomString(10),
+  // data10: randomString(10),
  }));
 
 console.dir(DATA[0], {depth: null});
@@ -123,26 +132,40 @@ console.dir(z3Union.parse(DATA[0]), {depth: null});
 console.dir(z3DiscUnion.parse(DATA[0]), {depth: null});
 console.dir(z4Union.parse(DATA[0]), {depth: null});
 console.dir(z4DiscUnion.parse(DATA[0]), {depth: null});
+console.dir(z4LibUnion.parse(DATA[0]), {depth: null});
+console.dir(z4LibDiscUnion.parse(DATA[0]), {depth: null});
 
+
+const args=  {jitless: true}
 const bench = metabench("z.disriminatedUnion().parse", {
-  z3() {
-    for (const item of DATA) {
-      z3Union.parse(item);
-    }
-  },
+  // z3() {
+  //   for (const item of DATA) {
+  //     z3Union.parse(item);
+  //   }
+  // },
   z3Disc() {
     for (const item of DATA) {
       z3DiscUnion.parse(item);
     }
   },
-  z4() {
+  // z4() {
+  //   for (const item of DATA) {
+  //     z4Union.parse(item);
+  //   }
+  // },
+  // z4jitless() {
+  //   for (const item of DATA) {
+  //     z4Union.parse(item, args);
+  //   }
+  // },
+  z4Disc() {
     for (const item of DATA) {
-      z4Union.parse(item);
+      z4DiscUnion.parse(item);
     }
   },
-  z4Disc() {
+  z4LibDisc(){
     for (const item of DATA) {
-      z4DiscUnion.parse(item);
+      z4LibDiscUnion.parse(item);
     }
   }
 })
diff --git a/packages/bench/jit-union.ts b/packages/bench/jit-union.ts
new file mode 100644
index 00000000..92f3c857
--- /dev/null
+++ b/packages/bench/jit-union.ts
@@ -0,0 +1,92 @@
+import { makeData, randomPick, randomString } from "./benchUtil.js";
+import { metabench } from "./metabench.js";
+
+import * as z4 from "zod/v4-mini";
+
+const z4fields = {
+  data1: z4.string(),
+  data2: z4.string(),
+  data3: z4.string(),
+  data4: z4.string(),
+  data5: z4.string(),
+  data6: z4.string(),
+  data7: z4.string(),
+  data8: z4.string(),
+  data9: z4.string(),
+  data10: z4.string(),
+}
+const z4Union = z4.union([
+  z4.object({
+    type: z4.literal("a"),
+    ...z4fields
+  }),
+  z4.object({
+    type: z4.literal("b"),
+    ...z4fields
+  }),
+  z4.object({
+    type: z4.literal("c"),
+    ...z4fields
+  }),
+  z4.object({
+    type: z4.literal("d"),
+    ...z4fields
+  }),
+  z4.object({
+    type: z4.literal("e"),
+    ...z4fields
+  }),
+  z4.object({
+    type: z4.literal("f"),
+    ...z4fields
+  }),
+  z4.object({
+    type: z4.literal("g"),
+    ...z4fields
+  }),
+]);
+
+
+
+const DATA = makeData(100, () => ({ 
+  type: randomPick([
+    "a", 
+    "b",
+    "c",
+    "d",
+    "e",
+    "f",
+    "g"
+  ]), 
+  data1: randomString(10),
+  data2: randomString(10),
+  data3: randomString(10),
+  data4: randomString(10),
+  data5: randomString(10),
+  data6: randomString(10),
+  data7: randomString(10),
+  data8: randomString(10),
+  data9: randomString(10),
+  data10: randomString(10),
+ }));
+
+
+const args=  {jitless: true}
+console.dir(z4Union.parse(DATA[0]), {depth: null});
+console.dir(z4Union.parse(DATA[0], args), {depth: null});
+const bench = metabench("z.disriminatedUnion().parse", {
+
+  v4_jit() {
+    for (const item of DATA) {
+      z4Union.parse(item);
+    }
+  },
+  v4_jitless() {
+    for (const item of DATA) {
+      z4Union.parse(item,args);
+    }
+  },
+  
+})
+  
+await bench.run();
diff --git a/packages/bench/package.json b/packages/bench/package.json
index 557ba1ac..466baa9e 100644
--- a/packages/bench/package.json
+++ b/packages/bench/package.json
@@ -6,6 +6,7 @@
     "arktype": "^2.1.19",
     "valibot": "^1.0.0",
     "zod": "workspace:*",
+    "zodnext": "npm:zod@next",
     "zod3": "npm:zod@^3.23.7"
   },
   "scripts": {
diff --git a/packages/docs/content/api.mdx b/packages/docs/content/api.mdx
index 236f7bac..bd735589 100644
--- a/packages/docs/content/api.mdx
+++ b/packages/docs/content/api.mdx
@@ -1392,13 +1392,13 @@ So Zod provides a `z.discriminatedUnion()` API that uses a *discriminator key* t
 
 
 ```ts
-const MyResult = z.discriminatedUnion([
+const MyResult = z.discriminatedUnion("status", [
   z.object({ status: z.literal("success"), data: z.string() }),
   z.object({ status: z.literal("failed"), error: z.string() }),
 ]);
 ```
 
-
+{/* 
 <Callout>
   In Zod 3, you were required to specify the discriminator key as the first argument. This is no longer necessary, as Zod can now automatically detect the discriminator key.
 
@@ -1410,7 +1410,7 @@ const MyResult = z.discriminatedUnion([
   ```
 
   If Zod can't find a discriminator key, it will throw an error at schema creation time.
-</Callout>
+</Callout> */}
 
 <Accordions type="single">
 <Accordion title="Nesting discriminated unions">
@@ -1419,13 +1419,13 @@ const MyResult = z.discriminatedUnion([
 
   ```ts
   const BaseError = { status: z.literal("failed"), message: z.string() };
-  const MyErrors = z.discriminatedUnion([
+  const MyErrors = z.discriminatedUnion("code", [
     z.object({ ...BaseError, code: z.literal(400) }),
     z.object({ ...BaseError, code: z.literal(401) }),
     z.object({ ...BaseError, code: z.literal(500) }),
   ]);
 
-  const MyResult = z.discriminatedUnion([
+  const MyResult = z.discriminatedUnion("status", [
     z.object({ status: z.literal("success"), data: z.string() }),
     MyErrors
   ]);
diff --git a/packages/zod/src/v4/classic/schemas.ts b/packages/zod/src/v4/classic/schemas.ts
index 5e9dcea6..b8ca8970 100644
--- a/packages/zod/src/v4/classic/schemas.ts
+++ b/packages/zod/src/v4/classic/schemas.ts
@@ -1266,22 +1266,17 @@ export interface $ZodTypeDiscriminable extends ZodType {
 }
 
 export function discriminatedUnion<Types extends readonly [$ZodTypeDiscriminable, ...$ZodTypeDiscriminable[]]>(
-  disc: string,
+  discriminator: string,
   options: Types,
   params?: string | core.$ZodDiscriminatedUnionParams
-): ZodDiscriminatedUnion<Types>;
-export function discriminatedUnion<Types extends readonly [$ZodTypeDiscriminable, ...$ZodTypeDiscriminable[]]>(
-  options: Types,
-  params?: string | core.$ZodDiscriminatedUnionParams
-): ZodDiscriminatedUnion<Types>;
-export function discriminatedUnion(...args: any[]): any {
-  if (typeof args[0] === "string") args = args.slice(1);
-  const [options, params] = args;
+): ZodDiscriminatedUnion<Types> {
+  // const [options, params] = args;
   return new ZodDiscriminatedUnion({
     type: "union",
     options,
+    discriminator,
     ...util.normalizeParams(params),
-  });
+  }) as any;
 }
 
 // ZodIntersection
diff --git a/packages/zod/src/v4/core/api.ts b/packages/zod/src/v4/core/api.ts
index 09871304..cafe9d44 100644
--- a/packages/zod/src/v4/core/api.ts
+++ b/packages/zod/src/v4/core/api.ts
@@ -1077,15 +1077,17 @@ export interface $ZodTypeDiscriminableInternals extends schemas.$ZodTypeInternal
 export interface $ZodTypeDiscriminable extends schemas.$ZodType {
   _zod: $ZodTypeDiscriminableInternals;
 }
-export type $ZodDiscriminatedUnionParams = TypeParams<schemas.$ZodDiscriminatedUnion, "options">;
+export type $ZodDiscriminatedUnionParams = TypeParams<schemas.$ZodDiscriminatedUnion, "options" | "discriminator">;
 export function _discriminatedUnion<Types extends [$ZodTypeDiscriminable, ...$ZodTypeDiscriminable[]]>(
   Class: util.SchemaClass<schemas.$ZodDiscriminatedUnion>,
+  discriminator: string,
   options: Types,
   params?: string | $ZodDiscriminatedUnionParams
 ): schemas.$ZodDiscriminatedUnion<Types> {
   return new Class({
     type: "union",
     options,
+    discriminator,
     ...util.normalizeParams(params),
   }) as any;
 }
diff --git a/packages/zod/src/v4/core/doc.ts b/packages/zod/src/v4/core/doc.ts
index 9591fe7a..9cb6a508 100644
--- a/packages/zod/src/v4/core/doc.ts
+++ b/packages/zod/src/v4/core/doc.ts
@@ -39,6 +39,7 @@ export class Doc {
     const content = this?.content ?? [``];
     const lines = [...content.map((x) => `  ${x}`)];
     // console.log(lines.join("\n"));
+    // console.dir("COMPILE", {depth: null});
     return new F(...args, lines.join("\n"));
   }
 }
diff --git a/packages/zod/src/v4/core/schemas.ts b/packages/zod/src/v4/core/schemas.ts
index 401538c8..87644878 100644
--- a/packages/zod/src/v4/core/schemas.ts
+++ b/packages/zod/src/v4/core/schemas.ts
@@ -1949,7 +1949,7 @@ export const $ZodUnion: core.$constructor<$ZodUnion> = /*@__PURE__*/ core.$const
   });
 
   inst._zod.parse = (payload, ctx) => {
-    const async = false;
+    let async = false;
 
     const results: util.MaybeAsync<ParsePayload>[] = [];
     for (const option of def.options) {
@@ -1962,6 +1962,7 @@ export const $ZodUnion: core.$constructor<$ZodUnion> = /*@__PURE__*/ core.$const
       );
       if (result instanceof Promise) {
         results.push(result);
+        async = true;
       } else {
         if (result.issues.length === 0) return result;
         results.push(result);
@@ -1985,6 +1986,7 @@ export const $ZodUnion: core.$constructor<$ZodUnion> = /*@__PURE__*/ core.$const
 
 export interface $ZodDiscriminatedUnionDef<Options extends readonly $ZodType[] = readonly $ZodType[]>
   extends $ZodUnionDef<Options> {
+  discriminator: string;
   unionFallback?: boolean;
 }
 
@@ -2054,13 +2056,14 @@ export const $ZodDiscriminatedUnion: core.$constructor<$ZodDiscriminatedUnion> =
       return _disc;
     });
 
-    const _discKeys = util.cached(() => {
-      const subdiscs = def.options.map((o) => o._zod.disc!);
-      const discKeys = [...inst._zod.disc.keys()].filter((k) => subdiscs.every((d) => d.has(k)));
-      if (discKeys.length === 0) {
-        throw new Error(`Invalid discriminated union: no shared discriminator key: ${discKeys.join(", ")}`);
+    const _discmap = util.cached(() => {
+      const map: Map<$ZodType, util.DiscriminatorMapElement> = new Map();
+      for (const o of def.options) {
+        const discEl = o._zod.disc?.get(def.discriminator);
+        if (!discEl) throw new Error("Invalid discriminated union option");
+        map.set(o, discEl);
       }
-      return discKeys;
+      return map;
     });
 
     inst._zod.parse = (payload, ctx) => {
@@ -2076,36 +2079,15 @@ export const $ZodDiscriminatedUnion: core.$constructor<$ZodDiscriminatedUnion> =
       }
 
       const filtered: $ZodType[] = [];
+      const discmap = _discmap.value;
       for (const option of def.options) {
-        if (matchDiscriminators(input, option._zod.disc!)) {
+        const subdisc = discmap.get(option)!;
+        if (matchDiscriminatorAtKey(input, def.discriminator, subdisc)) {
           filtered.push(option);
         }
-        // let matched = true;
-        // for (const k of _discKeys.value) {
-        //   if (!matchDiscriminatorAtKey(input, k, option._zod.disc!.get(k)!)) {
-        //     matched = false;
-        //   }
-        // }
-
-        // if (matched) {
-        //   filtered.push(option);
-        // } else if (!def.unionFallback) {
-        //   // if(matched)
-        //   // no matching discriminator
-        //   payload.issues.push({
-        //     code: "invalid_union",
-        //     errors: [],
-        //     note: "No matching discriminator",
-        //     path: [_discKeys.value[0]],
-        //     input,
-        //     inst,
-        //   });
-        // } else {
-        // }
       }
 
       if (filtered.length === 1) return filtered[0]._zod.run(payload, ctx) as any;
-
       if (def.unionFallback) {
         return _super(payload, ctx);
       }
@@ -2116,7 +2098,7 @@ export const $ZodDiscriminatedUnion: core.$constructor<$ZodDiscriminatedUnion> =
         errors: [],
         note: "No matching discriminator",
         input,
-        path: [_discKeys.value[0]],
+        path: [def.discriminator],
         inst,
       });
 
diff --git a/packages/zod/src/v4/mini/schemas.ts b/packages/zod/src/v4/mini/schemas.ts
index f8cb8a4f..d4b9b9a4 100644
--- a/packages/zod/src/v4/mini/schemas.ts
+++ b/packages/zod/src/v4/mini/schemas.ts
@@ -900,12 +900,14 @@ export interface $ZodTypeDiscriminable extends ZodMiniType {
 }
 
 export function discriminatedUnion<Types extends readonly [$ZodTypeDiscriminable, ...$ZodTypeDiscriminable[]]>(
+  discriminator: string,
   options: Types,
   params?: string | core.$ZodDiscriminatedUnionParams
 ): ZodMiniDiscriminatedUnion<Types> {
   return new ZodMiniDiscriminatedUnion({
     type: "union",
     options,
+    discriminator,
     ...util.normalizeParams(params),
   }) as ZodMiniDiscriminatedUnion<Types>;
 }
diff --git a/play.ts b/play.ts
index 54caaf7c..3e4bd90d 100644
--- a/play.ts
+++ b/play.ts
@@ -1,9 +1,3 @@
 import * as z from "zod/v4";
 
-const A = z.string().meta({ a: true });
-console.dir(A.meta(), { depth: null });
-const B = A.meta({ b: true });
-console.dir(B.meta(), { depth: null });
-const C = B.describe("hello");
-console.dir(C.meta(), { depth: null });
-// console.dir(C._zod.parent._zodreg, { depth: null });
+z;
\ No newline at end of file
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a1d247da..9cdced32 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -101,6 +101,9 @@ importers:
       zod3:
         specifier: npm:zod@^3.23.7
         version: zod@3.24.2
+      zodnext:
+        specifier: npm:zod@next
+        version: zod@3.25.0-beta.20250518T204520
 
   packages/docs:
     dependencies:
@@ -5451,6 +5454,9 @@ packages:
   zod@3.24.3:
     resolution: {integrity: sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==}
 
+  zod@3.25.0-beta.20250518T204520:
+    resolution: {integrity: sha512-R1E2HPJQ/VbG/P3Resy9+Oqgal1NNeAJMRmwqmlV/NlrpFjbk0hUKD9PAiZ9Pp5F/dQyCJmozNNSJZDDoikhxQ==}
+
   zod@4.0.0-beta.20250420T053007:
     resolution: {integrity: sha512-5pp8Q0PNDaNcUptGiBE9akyioJh3RJpagIxrLtAVMR9IxwcSZiOsJD/1/98CyhItdTlI2H91MfhhLzRlU+fifA==}
 
@@ -11081,6 +11087,8 @@ snapshots:
 
   zod@3.24.3: {}
 
+  zod@3.25.0-beta.20250518T204520: {}
+
   zod@4.0.0-beta.20250420T053007:
     dependencies:
       '@zod/core': 0.8.1


Test patch:
diff --git a/packages/zod/src/v4/classic/tests/discriminated-unions.test.ts b/packages/zod/src/v4/classic/tests/discriminated-unions.test.ts
index 72fd44a5..544e75f7 100644
--- a/packages/zod/src/v4/classic/tests/discriminated-unions.test.ts
+++ b/packages/zod/src/v4/classic/tests/discriminated-unions.test.ts
@@ -29,7 +29,7 @@ test("_values", () => {
 test("valid parse - object", () => {
   expect(
     z
-      .discriminatedUnion([
+      .discriminatedUnion("type", [
         z.object({ type: z.literal("a"), a: z.string() }),
         z.object({ type: z.literal("b"), b: z.string() }),
       ])
@@ -49,7 +49,7 @@ test("valid - include discriminator key (deprecated)", () => {
 });
 
 test("valid - optional discriminator (object)", () => {
-  const schema = z.discriminatedUnion([
+  const schema = z.discriminatedUnion("type", [
     z.object({ type: z.literal("a").optional(), a: z.string() }),
     z.object({ type: z.literal("b"), b: z.string() }),
   ]);
@@ -58,7 +58,7 @@ test("valid - optional discriminator (object)", () => {
 });
 
 test("valid - discriminator value of various primitive types", () => {
-  const schema = z.discriminatedUnion([
+  const schema = z.discriminatedUnion("type", [
     z.object({ type: z.literal("1"), val: z.string() }),
     z.object({ type: z.literal(1), val: z.string() }),
     z.object({ type: z.literal(BigInt(1)), val: z.string() }),
@@ -110,7 +110,7 @@ test("valid - discriminator value of various primitive types", () => {
 
 test("invalid - null", () => {
   try {
-    z.discriminatedUnion([
+    z.discriminatedUnion("type", [
       z.object({ type: z.literal("a"), a: z.string() }),
       z.object({ type: z.literal("b"), b: z.string() }),
     ]).parse(null);
@@ -141,7 +141,7 @@ test("invalid - null", () => {
 
 test("invalid discriminator value", () => {
   const result = z
-    .discriminatedUnion([
+    .discriminatedUnion("type", [
       z.object({ type: z.literal("a"), a: z.string() }),
       z.object({ type: z.literal("b"), b: z.string() }),
     ])
@@ -170,6 +170,7 @@ test("invalid discriminator value", () => {
 test("invalid discriminator value - unionFallback", () => {
   const result = z
     .discriminatedUnion(
+      "type",
       [z.object({ type: z.literal("a"), a: z.string() }), z.object({ type: z.literal("b"), b: z.string() })],
       { unionFallback: true }
     )
@@ -226,7 +227,7 @@ test("invalid discriminator value - unionFallback", () => {
 
 test("valid discriminator value, invalid data", () => {
   const result = z
-    .discriminatedUnion([
+    .discriminatedUnion("type", [
       z.object({ type: z.literal("a"), a: z.string() }),
       z.object({ type: z.literal("b"), b: z.string() }),
     ])
@@ -262,8 +263,10 @@ test("valid discriminator value, invalid data", () => {
 
 test("wrong schema - missing discriminator", () => {
   try {
-    z.discriminatedUnion([z.object({ type: z.literal("a"), a: z.string() }), z.object({ b: z.string() }) as any])._zod
-      .disc;
+    z.discriminatedUnion("type", [
+      z.object({ type: z.literal("a"), a: z.string() }),
+      z.object({ b: z.string() }) as any,
+    ])._zod.disc;
     throw new Error();
   } catch (e: any) {
     expect(e.message.includes("Invalid discriminated union option")).toBe(true);
@@ -273,7 +276,7 @@ test("wrong schema - missing discriminator", () => {
 // removed to account for unions of unions
 // test("wrong schema - duplicate discriminator values", () => {
 //   try {
-//     z.discriminatedUnion([
+//     z.discriminatedUnion("type",[
 //       z.object({ type: z.literal("a"), a: z.string() }),
 //       z.object({ type: z.literal("a"), b: z.string() }),
 //     ]);
@@ -284,7 +287,7 @@ test("wrong schema - missing discriminator", () => {
 // });
 
 test("async - valid", async () => {
-  const schema = await z.discriminatedUnion([
+  const schema = await z.discriminatedUnion("type", [
     z.object({
       type: z.literal("a"),
       a: z
@@ -304,7 +307,7 @@ test("async - valid", async () => {
 
 test("async - invalid", async () => {
   // try {
-  const a = z.discriminatedUnion([
+  const a = z.discriminatedUnion("type", [
     z.object({
       type: z.literal("a"),
       a: z
@@ -346,7 +349,7 @@ test("async - invalid", async () => {
 });
 
 test("valid - literals with .default or .pipe", () => {
-  const schema = z.discriminatedUnion([
+  const schema = z.discriminatedUnion("type", [
     z.object({
       type: z.literal("foo").default("foo"),
       a: z.string(),
@@ -469,7 +472,7 @@ test("multple discriminators", () => {
     min_cents: z.null(),
   });
 
-  const Config = z.discriminatedUnion([FreeConfig, PricedConfig]);
+  const Config = z.discriminatedUnion("type", [FreeConfig, PricedConfig]);
 
   Config.parse({
     min_cents: null,
@@ -501,7 +504,7 @@ test("single element union", () => {
 
   // Validation must fail here, but it doesn't
 
-  const u = z.discriminatedUnion([schema]);
+  const u = z.discriminatedUnion("a", [schema]);
   const result = u.safeParse(input);
   expect(result).toMatchObject({ success: false });
   expect(result).toMatchInlineSnapshot(`
diff --git a/packages/zod/src/v4/classic/tests/index.test.ts b/packages/zod/src/v4/classic/tests/index.test.ts
index e4ea4dd3..ad0132cc 100644
--- a/packages/zod/src/v4/classic/tests/index.test.ts
+++ b/packages/zod/src/v4/classic/tests/index.test.ts
@@ -243,7 +243,7 @@ test("z.discriminatedUnion", () => {
     age: z.number(),
   });
 
-  const c = z.discriminatedUnion([a, b]);
+  const c = z.discriminatedUnion("type", [a, b]);
 
   expect(c._zod.def.options.length).toEqual(2);
   expect(c._zod.disc!.get("type")!.values.has("A")).toEqual(true);
@@ -267,7 +267,7 @@ test("z.discriminatedUnion with nested discriminator", () => {
     age: z.number(),
   });
 
-  const c = z.discriminatedUnion([a, b]);
+  const c = z.discriminatedUnion("type", [a, b]);
   expect(c._zod.disc!.get("type")!.maps[0].get("key")!.values.has("A")).toEqual(true);
   expect(c._zod.disc!.get("type")!.maps[1].get("key")!.values.has("B")).toEqual(true);
 
@@ -282,7 +282,7 @@ test("z.discriminatedUnion with nested discriminator", () => {
 });
 
 test("z.discriminatedUnion nested", () => {
-  const schema1 = z.discriminatedUnion([
+  const schema1 = z.discriminatedUnion("type", [
     z.object({
       type: z.literal("A"),
       name: z.string(),
@@ -294,7 +294,7 @@ test("z.discriminatedUnion nested", () => {
     }),
   ]);
 
-  const schema2 = z.discriminatedUnion([
+  const schema2 = z.discriminatedUnion("type", [
     z.object({
       num: z.literal(2),
       type: z.literal("C"),
@@ -307,7 +307,7 @@ test("z.discriminatedUnion nested", () => {
     }),
   ]);
 
-  const hyper = z.discriminatedUnion([schema1, schema2]);
+  const hyper = z.discriminatedUnion("type", [schema1, schema2]);
   expect(hyper._zod.disc!.get("num")).toEqual({
     values: new Set([1, 2]),
     maps: [],
diff --git a/packages/zod/src/v4/mini/tests/index.test.ts b/packages/zod/src/v4/mini/tests/index.test.ts
index f19c83e6..d92d9cec 100644
--- a/packages/zod/src/v4/mini/tests/index.test.ts
+++ b/packages/zod/src/v4/mini/tests/index.test.ts
@@ -243,7 +243,7 @@ test("z.discriminatedUnion", () => {
     age: z.number(),
   });
 
-  const c = z.discriminatedUnion([a, b]);
+  const c = z.discriminatedUnion("type", [a, b]);
 
   expect(c._zod.def.options.length).toEqual(2);
   expect(c._zod.disc.get("type")!.values.has("A")).toEqual(true);
@@ -267,7 +267,7 @@ test("z.discriminatedUnion with nested discriminator", () => {
     age: z.number(),
   });
 
-  const c = z.discriminatedUnion([a, b]);
+  const c = z.discriminatedUnion("type", [a, b]);
   expect(c._zod.disc!.get("type")!.maps[0].get("key")!.values.has("A")).toEqual(true);
   expect(c._zod.disc!.get("type")!.maps[1].get("key")!.values.has("B")).toEqual(true);
 
@@ -282,7 +282,7 @@ test("z.discriminatedUnion with nested discriminator", () => {
 });
 
 test("z.discriminatedUnion nested", () => {
-  const schema1 = z.discriminatedUnion([
+  const schema1 = z.discriminatedUnion("type", [
     z.object({
       type: z.literal("A"),
       name: z.string(),
@@ -294,7 +294,7 @@ test("z.discriminatedUnion nested", () => {
     }),
   ]);
 
-  const schema2 = z.discriminatedUnion([
+  const schema2 = z.discriminatedUnion("type", [
     z.object({
       num: z.literal(2),
       type: z.literal("C"),
@@ -307,7 +307,7 @@ test("z.discriminatedUnion nested", () => {
     }),
   ]);
 
-  const hyper = z.discriminatedUnion([schema1, schema2]);
+  const hyper = z.discriminatedUnion("type", [schema1, schema2]);
   expect(hyper._zod.disc.get("num")).toEqual({
     values: new Set([1, 2]),
     maps: [],