Skip to content

Commit aa1049f

Browse files
feat: allow zod discriminatedUnion generation (#3698)
1 parent 996a4dd commit aa1049f

11 files changed

Lines changed: 731 additions & 101 deletions

File tree

docs/content/docs/guides/zod.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ export default defineConfig({
189189

190190
Per-operation and per-tag overrides accept the settings that apply to an individual schema: `strict`, `generate`, `coerce`, `preprocess`, `params`, and `useBrandedTypes`.
191191

192-
Output-wide settings — `variant`, `version`, `dateTimeOptions`, `timeOptions`, `generateEachHttpStatus`, `generateReusableSchemas`, and `generateMeta` — only make sense for the whole output and must stay on `override.zod`. If you place one on an operation or tag it is ignored — the value from `override.zod` still applies — and Orval prints a build warning:
192+
Output-wide settings — `variant`, `version`, `dateTimeOptions`, `timeOptions`, `generateEachHttpStatus`, `generateReusableSchemas`, `generateMeta`, and `generateDiscriminatedUnion` — only make sense for the whole output and must stay on `override.zod`. If you place one on an operation or tag it is ignored — the value from `override.zod` still applies — and Orval prints a build warning:
193193

194194
```
195195
⚠️ override.operations.listPets.zod only supports strict, generate, coerce, preprocess, params, and useBrandedTypes. Ignoring unsupported field: zod.version.

docs/content/docs/reference/configuration/output.mdx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1401,6 +1401,7 @@ export default defineConfig({
14011401
generateEachHttpStatus: true,
14021402
useBrandedTypes: true,
14031403
generateReusableSchemas: true,
1404+
generateDiscriminatedUnion: true,
14041405
},
14051406
},
14061407
},
@@ -1574,6 +1575,29 @@ Behavior:
15741575
- **zod v3** has no `.meta()` — the option is a no-op there, and descriptions continue to emit via `.describe()`.
15751576
- The registry `id` makes [`z.toJSONSchema()`](https://zod.dev/json-schema) reference the schema as `#/$defs/<id>`, round-tripping the component structure.
15761577

1578+
### generateDiscriminatedUnion
1579+
1580+
**Type:** `boolean`
1581+
1582+
**Default:** `false`
1583+
1584+
Emit a `oneOf`/`anyOf` that carries an OpenAPI [`discriminator`](https://spec.openapis.org/oas/v3.1.0#discriminator-object) as [`zod.discriminatedUnion(key, [...])`](https://zod.dev/api?id=discriminated-unions) instead of a plain `zod.union([...])`. A discriminated union picks the branch by its discriminator value first, so validation errors point at the offending field (`type.name`) instead of collapsing into a single "no union member matched" at the union root.
1585+
1586+
```ts
1587+
// override: { zod: { generateDiscriminatedUnion: true } }
1588+
export const Pet = zod.discriminatedUnion('petType', [
1589+
zod.object({ petType: zod.literal('cat'), meows: zod.boolean() }),
1590+
zod.object({ petType: zod.literal('dog'), barks: zod.boolean() }),
1591+
]);
1592+
```
1593+
1594+
Behavior:
1595+
1596+
- **Opt-in.** Left `false`, unions are emitted exactly as before, so existing output is unchanged.
1597+
- **Safe fallback.** A discriminated union is emitted only when every branch can be represented as an object carrying a literal (`const`/`enum`) discriminator. If any branch is a non-object, a nested union, or lacks a literal discriminator, generation falls back to a plain `zod.union([...])` rather than emitting code that throws at construction.
1598+
- **Inheritance (`allOf`).** Branches composed with `allOf` are flattened into a single object so they remain valid discriminated-union options — this is the case that previously forced the feature to be reverted ([#2085](https://github.com/orval-labs/orval/issues/2085)). With [`generateReusableSchemas`](#generatereusableschemas), a branch that references an `allOf` schema stays a plain union (the referenced schema can't be guaranteed to be an object from the reference alone).
1599+
- Works with both Zod v3 (>= 3.20) and v4, and with the [`mini`](#variant) variant.
1600+
15771601
---
15781602

15791603
## override.effect

packages/angular/src/http-client.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ const createOutput = (
100100
generateEachHttpStatus: false,
101101
generateReusableSchemas: false,
102102
generateMeta: false,
103+
generateDiscriminatedUnion: false,
103104
useBrandedTypes: false,
104105
dateTimeOptions: {},
105106
timeOptions: {},

packages/angular/src/http-resource.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ const createOutput = (
111111
useBrandedTypes: false,
112112
generateReusableSchemas: false,
113113
generateMeta: false,
114+
generateDiscriminatedUnion: false,
114115
dateTimeOptions: {},
115116
timeOptions: {},
116117
},

packages/core/src/test-utils/context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ export function createTestContextSpec({
132132
useBrandedTypes: false,
133133
generateReusableSchemas: false,
134134
generateMeta: false,
135+
generateDiscriminatedUnion: false,
135136
dateTimeOptions: {},
136137
timeOptions: { precision: 3 },
137138
},

packages/core/src/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,14 @@ export interface ZodOptions extends BaseZodOptions {
884884
* via `.describe()`. Default `false`.
885885
*/
886886
generateMeta?: boolean;
887+
/**
888+
* When true, a `oneOf`/`anyOf` that carries an OpenAPI `discriminator` is
889+
* emitted as `zod.discriminatedUnion(key, [...])` (better per-branch errors)
890+
* instead of a plain `zod.union([...])`, but only when every branch can be
891+
* represented as an object — otherwise it safely falls back to a union.
892+
* Default `false` (opt-in), so existing output is unchanged.
893+
*/
894+
generateDiscriminatedUnion?: boolean;
887895
}
888896

889897
/**
@@ -948,6 +956,7 @@ export interface NormalizedZodOptions {
948956
useBrandedTypes: boolean;
949957
generateReusableSchemas: boolean;
950958
generateMeta: boolean;
959+
generateDiscriminatedUnion: boolean;
951960
dateTimeOptions: ZodDateTimeOptions;
952961
timeOptions: ZodTimeOptions;
953962
}

packages/mock/src/faker/getters/combine.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ function createMockContext(): ContextSpec {
128128
useBrandedTypes: false,
129129
generateReusableSchemas: false,
130130
generateMeta: false,
131+
generateDiscriminatedUnion: false,
131132
dateTimeOptions: {},
132133
timeOptions: { precision: 3 },
133134
},

packages/orval/src/utils/options.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,8 @@ export async function normalizeOptions(
657657
generateReusableSchemas:
658658
outputOptions.override?.zod?.generateReusableSchemas ?? false,
659659
generateMeta: outputOptions.override?.zod?.generateMeta ?? false,
660+
generateDiscriminatedUnion:
661+
outputOptions.override?.zod?.generateDiscriminatedUnion ?? false,
660662
dateTimeOptions: outputOptions.override?.zod?.dateTimeOptions ?? {
661663
offset: true,
662664
},
@@ -993,6 +995,7 @@ function normalizeOperationsAndTags(
993995
'generateEachHttpStatus',
994996
'generateReusableSchemas',
995997
'generateMeta',
998+
'generateDiscriminatedUnion',
996999
] as const;
9971000

9981001
return Object.fromEntries(

packages/solid-start/src/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ function makeOutput(useDates = false): ContextSpec['output'] {
133133
useBrandedTypes: false,
134134
generateReusableSchemas: false,
135135
generateMeta: false,
136+
generateDiscriminatedUnion: false,
136137
dateTimeOptions: {},
137138
timeOptions: { precision: 3 },
138139
},

0 commit comments

Comments
 (0)