diff --git a/.changeset/fix-zod-discriminated-union-multi-value.md b/.changeset/fix-zod-discriminated-union-multi-value.md new file mode 100644 index 0000000000..ffba685734 --- /dev/null +++ b/.changeset/fix-zod-discriminated-union-multi-value.md @@ -0,0 +1,5 @@ +--- +'@hey-api/openapi-ts': patch +--- + +**plugin(zod)**: fix: generate z.discriminatedUnion when discriminator maps multiple values to the same schema diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/discriminator-mapped-many-number/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/discriminator-mapped-many-number/zod.gen.ts new file mode 100644 index 0000000000..b3327ba5b3 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/discriminator-mapped-many-number/zod.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod/v4-mini'; + +export const zBar = z.object({ + code: z.optional(z.union([z.literal(1), z.literal(2)])) +}); + +export const zBaz = z.object({ + code: z.optional(z.literal(3)) +}); + +export const zFoo = z.discriminatedUnion('code', [ + z.extend(zBar, { code: z.union([z.literal(1), z.literal(2)]) }), + z.extend(zBaz, { code: z.literal(3) }) +]); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/discriminator-mapped-many/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/discriminator-mapped-many/zod.gen.ts new file mode 100644 index 0000000000..a7e388b534 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/discriminator-mapped-many/zod.gen.ts @@ -0,0 +1,21 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod/v4-mini'; + +export const zBar = z.object({ + foo: z.optional(z.enum(['one', 'two'])) +}); + +export const zBaz = z.object({ + foo: z.optional(z.enum(['three'])) +}); + +export const zSpæcial = z.object({ + foo: z.optional(z.enum(['four'])) +}); + +export const zFoo = z.discriminatedUnion('foo', [ + z.extend(zBar, { foo: z.enum(['one', 'two']) }), + z.extend(zBaz, { foo: z.literal('three') }), + z.extend(zSpæcial, { foo: z.literal('four') }) +]); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/discriminator-mapped-many-number/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/discriminator-mapped-many-number/zod.gen.ts new file mode 100644 index 0000000000..d8148660c1 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/discriminator-mapped-many-number/zod.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod'; + +export const zBar = z.object({ + code: z.union([z.literal(1), z.literal(2)]).optional() +}); + +export const zBaz = z.object({ + code: z.literal(3).optional() +}); + +export const zFoo = z.discriminatedUnion('code', [ + zBar.extend({ code: z.union([z.literal(1), z.literal(2)]) }), + zBaz.extend({ code: z.literal(3) }) +]); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/discriminator-mapped-many/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/discriminator-mapped-many/zod.gen.ts new file mode 100644 index 0000000000..5b51ab9abe --- /dev/null +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/discriminator-mapped-many/zod.gen.ts @@ -0,0 +1,21 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod'; + +export const zBar = z.object({ + foo: z.enum(['one', 'two']).optional() +}); + +export const zBaz = z.object({ + foo: z.enum(['three']).optional() +}); + +export const zSpæcial = z.object({ + foo: z.enum(['four']).optional() +}); + +export const zFoo = z.discriminatedUnion('foo', [ + zBar.extend({ foo: z.enum(['one', 'two']) }), + zBaz.extend({ foo: z.literal('three') }), + zSpæcial.extend({ foo: z.literal('four') }) +]); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/discriminator-mapped-many-number/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/discriminator-mapped-many-number/zod.gen.ts new file mode 100644 index 0000000000..0417778aea --- /dev/null +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/discriminator-mapped-many-number/zod.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod/v4'; + +export const zBar = z.object({ + code: z.union([z.literal(1), z.literal(2)]).optional() +}); + +export const zBaz = z.object({ + code: z.literal(3).optional() +}); + +export const zFoo = z.discriminatedUnion('code', [ + zBar.extend({ code: z.union([z.literal(1), z.literal(2)]) }), + zBaz.extend({ code: z.literal(3) }) +]); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/discriminator-mapped-many/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/discriminator-mapped-many/zod.gen.ts new file mode 100644 index 0000000000..9994b91a08 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/discriminator-mapped-many/zod.gen.ts @@ -0,0 +1,21 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod/v4'; + +export const zBar = z.object({ + foo: z.enum(['one', 'two']).optional() +}); + +export const zBaz = z.object({ + foo: z.enum(['three']).optional() +}); + +export const zSpæcial = z.object({ + foo: z.enum(['four']).optional() +}); + +export const zFoo = z.discriminatedUnion('foo', [ + zBar.extend({ foo: z.enum(['one', 'two']) }), + zBaz.extend({ foo: z.literal('three') }), + zSpæcial.extend({ foo: z.literal('four') }) +]); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/discriminator-mapped-many-number/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/discriminator-mapped-many-number/zod.gen.ts new file mode 100644 index 0000000000..b3327ba5b3 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/discriminator-mapped-many-number/zod.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod/v4-mini'; + +export const zBar = z.object({ + code: z.optional(z.union([z.literal(1), z.literal(2)])) +}); + +export const zBaz = z.object({ + code: z.optional(z.literal(3)) +}); + +export const zFoo = z.discriminatedUnion('code', [ + z.extend(zBar, { code: z.union([z.literal(1), z.literal(2)]) }), + z.extend(zBaz, { code: z.literal(3) }) +]); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/discriminator-mapped-many/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/discriminator-mapped-many/zod.gen.ts new file mode 100644 index 0000000000..a7e388b534 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/discriminator-mapped-many/zod.gen.ts @@ -0,0 +1,21 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod/v4-mini'; + +export const zBar = z.object({ + foo: z.optional(z.enum(['one', 'two'])) +}); + +export const zBaz = z.object({ + foo: z.optional(z.enum(['three'])) +}); + +export const zSpæcial = z.object({ + foo: z.optional(z.enum(['four'])) +}); + +export const zFoo = z.discriminatedUnion('foo', [ + z.extend(zBar, { foo: z.enum(['one', 'two']) }), + z.extend(zBaz, { foo: z.literal('three') }), + z.extend(zSpæcial, { foo: z.literal('four') }) +]); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/discriminator-mapped-many-number/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/discriminator-mapped-many-number/zod.gen.ts new file mode 100644 index 0000000000..d8148660c1 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/discriminator-mapped-many-number/zod.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod'; + +export const zBar = z.object({ + code: z.union([z.literal(1), z.literal(2)]).optional() +}); + +export const zBaz = z.object({ + code: z.literal(3).optional() +}); + +export const zFoo = z.discriminatedUnion('code', [ + zBar.extend({ code: z.union([z.literal(1), z.literal(2)]) }), + zBaz.extend({ code: z.literal(3) }) +]); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/discriminator-mapped-many/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/discriminator-mapped-many/zod.gen.ts new file mode 100644 index 0000000000..5b51ab9abe --- /dev/null +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/discriminator-mapped-many/zod.gen.ts @@ -0,0 +1,21 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod'; + +export const zBar = z.object({ + foo: z.enum(['one', 'two']).optional() +}); + +export const zBaz = z.object({ + foo: z.enum(['three']).optional() +}); + +export const zSpæcial = z.object({ + foo: z.enum(['four']).optional() +}); + +export const zFoo = z.discriminatedUnion('foo', [ + zBar.extend({ foo: z.enum(['one', 'two']) }), + zBaz.extend({ foo: z.literal('three') }), + zSpæcial.extend({ foo: z.literal('four') }) +]); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/discriminator-mapped-many-number/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/discriminator-mapped-many-number/zod.gen.ts new file mode 100644 index 0000000000..0417778aea --- /dev/null +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/discriminator-mapped-many-number/zod.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod/v4'; + +export const zBar = z.object({ + code: z.union([z.literal(1), z.literal(2)]).optional() +}); + +export const zBaz = z.object({ + code: z.literal(3).optional() +}); + +export const zFoo = z.discriminatedUnion('code', [ + zBar.extend({ code: z.union([z.literal(1), z.literal(2)]) }), + zBaz.extend({ code: z.literal(3) }) +]); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/discriminator-mapped-many/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/discriminator-mapped-many/zod.gen.ts new file mode 100644 index 0000000000..9994b91a08 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/discriminator-mapped-many/zod.gen.ts @@ -0,0 +1,21 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod/v4'; + +export const zBar = z.object({ + foo: z.enum(['one', 'two']).optional() +}); + +export const zBaz = z.object({ + foo: z.enum(['three']).optional() +}); + +export const zSpæcial = z.object({ + foo: z.enum(['four']).optional() +}); + +export const zFoo = z.discriminatedUnion('foo', [ + zBar.extend({ foo: z.enum(['one', 'two']) }), + zBaz.extend({ foo: z.literal('three') }), + zSpæcial.extend({ foo: z.literal('four') }) +]); diff --git a/packages/openapi-ts-tests/zod/v3/test/3.0.x.test.ts b/packages/openapi-ts-tests/zod/v3/test/3.0.x.test.ts index f498a05473..d2418c0cb3 100644 --- a/packages/openapi-ts-tests/zod/v3/test/3.0.x.test.ts +++ b/packages/openapi-ts-tests/zod/v3/test/3.0.x.test.ts @@ -34,6 +34,22 @@ for (const zodVersion of zodVersions) { }), description: 'generates circular schemas', }, + { + config: createConfig({ + input: 'discriminator-mapped-many.yaml', + output: 'discriminator-mapped-many', + }), + description: + 'generates discriminated union when multiple mapping values point to same schema', + }, + { + config: createConfig({ + input: 'discriminator-mapped-many-number.yaml', + output: 'discriminator-mapped-many-number', + }), + description: + 'generates discriminated union when multiple number mapping values point to same schema', + }, { config: createConfig({ input: 'enum-null.json', diff --git a/packages/openapi-ts-tests/zod/v3/test/3.1.x.test.ts b/packages/openapi-ts-tests/zod/v3/test/3.1.x.test.ts index 5761d3ba64..ed01c96e5f 100644 --- a/packages/openapi-ts-tests/zod/v3/test/3.1.x.test.ts +++ b/packages/openapi-ts-tests/zod/v3/test/3.1.x.test.ts @@ -152,6 +152,22 @@ for (const zodVersion of zodVersions) { }), description: 'handles oneOf discriminator (falls back to z.union when needed)', }, + { + config: createConfig({ + input: 'discriminator-mapped-many.yaml', + output: 'discriminator-mapped-many', + }), + description: + 'generates discriminated union when multiple mapping values point to same schema', + }, + { + config: createConfig({ + input: 'discriminator-mapped-many-number.yaml', + output: 'discriminator-mapped-many-number', + }), + description: + 'generates discriminated union when multiple number mapping values point to same schema', + }, { config: createConfig({ input: 'enum-null.json', diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/discriminator-mapped-many-number/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/discriminator-mapped-many-number/zod.gen.ts new file mode 100644 index 0000000000..5fc0bdc133 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/discriminator-mapped-many-number/zod.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod/mini'; + +export const zBar = z.object({ + code: z.optional(z.union([z.literal(1), z.literal(2)])) +}); + +export const zBaz = z.object({ + code: z.optional(z.literal(3)) +}); + +export const zFoo = z.discriminatedUnion('code', [ + z.extend(zBar, { code: z.union([z.literal(1), z.literal(2)]) }), + z.extend(zBaz, { code: z.literal(3) }) +]); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/discriminator-mapped-many/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/discriminator-mapped-many/zod.gen.ts new file mode 100644 index 0000000000..a365be0f13 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/discriminator-mapped-many/zod.gen.ts @@ -0,0 +1,21 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod/mini'; + +export const zBar = z.object({ + foo: z.optional(z.enum(['one', 'two'])) +}); + +export const zBaz = z.object({ + foo: z.optional(z.enum(['three'])) +}); + +export const zSpæcial = z.object({ + foo: z.optional(z.enum(['four'])) +}); + +export const zFoo = z.discriminatedUnion('foo', [ + z.extend(zBar, { foo: z.enum(['one', 'two']) }), + z.extend(zBaz, { foo: z.literal('three') }), + z.extend(zSpæcial, { foo: z.literal('four') }) +]); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/discriminator-mapped-many-number/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/discriminator-mapped-many-number/zod.gen.ts new file mode 100644 index 0000000000..da2629c131 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/discriminator-mapped-many-number/zod.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod/v3'; + +export const zBar = z.object({ + code: z.union([z.literal(1), z.literal(2)]).optional() +}); + +export const zBaz = z.object({ + code: z.literal(3).optional() +}); + +export const zFoo = z.discriminatedUnion('code', [ + zBar.extend({ code: z.union([z.literal(1), z.literal(2)]) }), + zBaz.extend({ code: z.literal(3) }) +]); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/discriminator-mapped-many/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/discriminator-mapped-many/zod.gen.ts new file mode 100644 index 0000000000..1b25c19cad --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/discriminator-mapped-many/zod.gen.ts @@ -0,0 +1,21 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod/v3'; + +export const zBar = z.object({ + foo: z.enum(['one', 'two']).optional() +}); + +export const zBaz = z.object({ + foo: z.enum(['three']).optional() +}); + +export const zSpæcial = z.object({ + foo: z.enum(['four']).optional() +}); + +export const zFoo = z.discriminatedUnion('foo', [ + zBar.extend({ foo: z.enum(['one', 'two']) }), + zBaz.extend({ foo: z.literal('three') }), + zSpæcial.extend({ foo: z.literal('four') }) +]); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/discriminator-mapped-many-number/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/discriminator-mapped-many-number/zod.gen.ts new file mode 100644 index 0000000000..b11c5e2390 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/discriminator-mapped-many-number/zod.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod'; + +export const zBar = z.object({ + code: z.union([z.literal(1), z.literal(2)]).optional() +}); + +export const zBaz = z.object({ + code: z.literal(3).optional() +}); + +export const zFoo = z.discriminatedUnion('code', [ + zBar.extend({ code: z.union([z.literal(1), z.literal(2)]) }), + zBaz.extend({ code: z.literal(3) }) +]); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/discriminator-mapped-many/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/discriminator-mapped-many/zod.gen.ts new file mode 100644 index 0000000000..facaac6e38 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/discriminator-mapped-many/zod.gen.ts @@ -0,0 +1,21 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod'; + +export const zBar = z.object({ + foo: z.enum(['one', 'two']).optional() +}); + +export const zBaz = z.object({ + foo: z.enum(['three']).optional() +}); + +export const zSpæcial = z.object({ + foo: z.enum(['four']).optional() +}); + +export const zFoo = z.discriminatedUnion('foo', [ + zBar.extend({ foo: z.enum(['one', 'two']) }), + zBaz.extend({ foo: z.literal('three') }), + zSpæcial.extend({ foo: z.literal('four') }) +]); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/discriminator-mapped-many-number/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/discriminator-mapped-many-number/zod.gen.ts new file mode 100644 index 0000000000..5fc0bdc133 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/discriminator-mapped-many-number/zod.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod/mini'; + +export const zBar = z.object({ + code: z.optional(z.union([z.literal(1), z.literal(2)])) +}); + +export const zBaz = z.object({ + code: z.optional(z.literal(3)) +}); + +export const zFoo = z.discriminatedUnion('code', [ + z.extend(zBar, { code: z.union([z.literal(1), z.literal(2)]) }), + z.extend(zBaz, { code: z.literal(3) }) +]); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/discriminator-mapped-many/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/discriminator-mapped-many/zod.gen.ts new file mode 100644 index 0000000000..a365be0f13 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/discriminator-mapped-many/zod.gen.ts @@ -0,0 +1,21 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod/mini'; + +export const zBar = z.object({ + foo: z.optional(z.enum(['one', 'two'])) +}); + +export const zBaz = z.object({ + foo: z.optional(z.enum(['three'])) +}); + +export const zSpæcial = z.object({ + foo: z.optional(z.enum(['four'])) +}); + +export const zFoo = z.discriminatedUnion('foo', [ + z.extend(zBar, { foo: z.enum(['one', 'two']) }), + z.extend(zBaz, { foo: z.literal('three') }), + z.extend(zSpæcial, { foo: z.literal('four') }) +]); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/discriminator-mapped-many-number/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/discriminator-mapped-many-number/zod.gen.ts new file mode 100644 index 0000000000..da2629c131 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/discriminator-mapped-many-number/zod.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod/v3'; + +export const zBar = z.object({ + code: z.union([z.literal(1), z.literal(2)]).optional() +}); + +export const zBaz = z.object({ + code: z.literal(3).optional() +}); + +export const zFoo = z.discriminatedUnion('code', [ + zBar.extend({ code: z.union([z.literal(1), z.literal(2)]) }), + zBaz.extend({ code: z.literal(3) }) +]); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/discriminator-mapped-many/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/discriminator-mapped-many/zod.gen.ts new file mode 100644 index 0000000000..1b25c19cad --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/discriminator-mapped-many/zod.gen.ts @@ -0,0 +1,21 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod/v3'; + +export const zBar = z.object({ + foo: z.enum(['one', 'two']).optional() +}); + +export const zBaz = z.object({ + foo: z.enum(['three']).optional() +}); + +export const zSpæcial = z.object({ + foo: z.enum(['four']).optional() +}); + +export const zFoo = z.discriminatedUnion('foo', [ + zBar.extend({ foo: z.enum(['one', 'two']) }), + zBaz.extend({ foo: z.literal('three') }), + zSpæcial.extend({ foo: z.literal('four') }) +]); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/discriminator-mapped-many-number/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/discriminator-mapped-many-number/zod.gen.ts new file mode 100644 index 0000000000..b11c5e2390 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/discriminator-mapped-many-number/zod.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod'; + +export const zBar = z.object({ + code: z.union([z.literal(1), z.literal(2)]).optional() +}); + +export const zBaz = z.object({ + code: z.literal(3).optional() +}); + +export const zFoo = z.discriminatedUnion('code', [ + zBar.extend({ code: z.union([z.literal(1), z.literal(2)]) }), + zBaz.extend({ code: z.literal(3) }) +]); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/discriminator-mapped-many/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/discriminator-mapped-many/zod.gen.ts new file mode 100644 index 0000000000..facaac6e38 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/discriminator-mapped-many/zod.gen.ts @@ -0,0 +1,21 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod'; + +export const zBar = z.object({ + foo: z.enum(['one', 'two']).optional() +}); + +export const zBaz = z.object({ + foo: z.enum(['three']).optional() +}); + +export const zSpæcial = z.object({ + foo: z.enum(['four']).optional() +}); + +export const zFoo = z.discriminatedUnion('foo', [ + zBar.extend({ foo: z.enum(['one', 'two']) }), + zBaz.extend({ foo: z.literal('three') }), + zSpæcial.extend({ foo: z.literal('four') }) +]); diff --git a/packages/openapi-ts-tests/zod/v4/test/3.0.x.test.ts b/packages/openapi-ts-tests/zod/v4/test/3.0.x.test.ts index 18659712b5..0d004290f3 100644 --- a/packages/openapi-ts-tests/zod/v4/test/3.0.x.test.ts +++ b/packages/openapi-ts-tests/zod/v4/test/3.0.x.test.ts @@ -42,6 +42,22 @@ for (const zodVersion of zodVersions) { description: 'falls back to z.union() when discriminated union members have allOf (intersection)', }, + { + config: createConfig({ + input: 'discriminator-mapped-many.yaml', + output: 'discriminator-mapped-many', + }), + description: + 'generates discriminated union when multiple mapping values point to same schema', + }, + { + config: createConfig({ + input: 'discriminator-mapped-many-number.yaml', + output: 'discriminator-mapped-many-number', + }), + description: + 'generates discriminated union when multiple number mapping values point to same schema', + }, { config: createConfig({ input: 'enum-null.json', diff --git a/packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts b/packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts index 703c51f7ab..3232387292 100644 --- a/packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts +++ b/packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts @@ -185,6 +185,22 @@ for (const zodVersion of zodVersions) { }), description: 'handles oneOf discriminator (falls back to z.union when needed)', }, + { + config: createConfig({ + input: 'discriminator-mapped-many.yaml', + output: 'discriminator-mapped-many', + }), + description: + 'generates discriminated union when multiple mapping values point to same schema', + }, + { + config: createConfig({ + input: 'discriminator-mapped-many-number.yaml', + output: 'discriminator-mapped-many-number', + }), + description: + 'generates discriminated union when multiple number mapping values point to same schema', + }, { config: createConfig({ input: 'enum-null.json', diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/union.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/union.ts index 183ce382c8..45cc711726 100644 --- a/packages/openapi-ts/src/plugins/zod/mini/toAst/union.ts +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/union.ts @@ -49,7 +49,19 @@ function baseNode(ctx: UnionResolverContext): Chain { member.refExpression, $.object().prop( discriminatedExpression.discriminatorKey, - $(z).attr(identifiers.literal).call($.fromValue(member.discriminatedValue)), + Array.isArray(member.discriminatedValue) + ? member.discriminatedValue.every((v) => typeof v === 'string') + ? $(z).attr(identifiers.enum).call($.fromValue(member.discriminatedValue)) + : $(z) + .attr(identifiers.union) + .call( + $.array( + ...member.discriminatedValue.map((v) => + $(z).attr(identifiers.literal).call($.fromValue(v)), + ), + ), + ) + : $(z).attr(identifiers.literal).call($.fromValue(member.discriminatedValue)), ), ), ); diff --git a/packages/openapi-ts/src/plugins/zod/shared/discriminated-union.ts b/packages/openapi-ts/src/plugins/zod/shared/discriminated-union.ts index 4e85cc1a13..f98153fe55 100644 --- a/packages/openapi-ts/src/plugins/zod/shared/discriminated-union.ts +++ b/packages/openapi-ts/src/plugins/zod/shared/discriminated-union.ts @@ -40,7 +40,16 @@ export function tryBuildDiscriminatedUnion({ if (schema.logicalOperator !== 'and' || !schema.items || schema.items.length !== 2) return null; const refPart = schema.items[1]!; - const discriminatedValue = schema.items[0]!.properties?.[discriminatorKey]?.const; + const discriminatorProp = schema.items[0]!.properties?.[discriminatorKey]; + let discriminatedValue: unknown; + if (discriminatorProp?.const !== undefined) { + discriminatedValue = discriminatorProp.const; + } else if ( + discriminatorProp?.logicalOperator === 'or' && + discriminatorProp.items?.every((item) => item.const !== undefined) + ) { + discriminatedValue = discriminatorProp.items.map((item) => item.const); + } // lazy references can't be used in a discriminated union directly if (discriminatedValue === undefined || items[index]!.meta.hasLazy) return null; diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/union.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/union.ts index d588b84f28..0536cbe3f9 100644 --- a/packages/openapi-ts/src/plugins/zod/v3/toAst/union.ts +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/union.ts @@ -43,14 +43,24 @@ function baseNode(ctx: UnionResolverContext): Chain { if (discriminatedExpression) { const unionMembers = discriminatedExpression.members.map((member) => - member.refExpression - .attr(identifiers.extend) - .call( - $.object().prop( - discriminatedExpression.discriminatorKey, - $(z).attr(identifiers.literal).call($.fromValue(member.discriminatedValue)), - ), + member.refExpression.attr(identifiers.extend).call( + $.object().prop( + discriminatedExpression.discriminatorKey, + Array.isArray(member.discriminatedValue) + ? member.discriminatedValue.every((v) => typeof v === 'string') + ? $(z).attr(identifiers.enum).call($.fromValue(member.discriminatedValue)) + : $(z) + .attr(identifiers.union) + .call( + $.array( + ...member.discriminatedValue.map((v) => + $(z).attr(identifiers.literal).call($.fromValue(v)), + ), + ), + ) + : $(z).attr(identifiers.literal).call($.fromValue(member.discriminatedValue)), ), + ), ); return $(z) diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/union.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/union.ts index 7ec7667344..a6e8078c11 100644 --- a/packages/openapi-ts/src/plugins/zod/v4/toAst/union.ts +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/union.ts @@ -41,14 +41,24 @@ function baseNode(ctx: UnionResolverContext): Chain { if (discriminatedExpression) { const unionMembers = discriminatedExpression.members.map((member) => - member.refExpression - .attr(identifiers.extend) - .call( - $.object().prop( - discriminatedExpression.discriminatorKey, - $(z).attr(identifiers.literal).call($.fromValue(member.discriminatedValue)), - ), + member.refExpression.attr(identifiers.extend).call( + $.object().prop( + discriminatedExpression.discriminatorKey, + Array.isArray(member.discriminatedValue) + ? member.discriminatedValue.every((v) => typeof v === 'string') + ? $(z).attr(identifiers.enum).call($.fromValue(member.discriminatedValue)) + : $(z) + .attr(identifiers.union) + .call( + $.array( + ...member.discriminatedValue.map((v) => + $(z).attr(identifiers.literal).call($.fromValue(v)), + ), + ), + ) + : $(z).attr(identifiers.literal).call($.fromValue(member.discriminatedValue)), ), + ), ); expression = $(z) diff --git a/specs/3.0.x/discriminator-mapped-many-number.yaml b/specs/3.0.x/discriminator-mapped-many-number.yaml new file mode 100644 index 0000000000..6c5fc9e8ba --- /dev/null +++ b/specs/3.0.x/discriminator-mapped-many-number.yaml @@ -0,0 +1,31 @@ +openapi: 3.0.0 +info: + title: OpenAPI 3.0.0 discriminator mapped many (number) example + version: 1 +components: + schemas: + Foo: + oneOf: + - $ref: '#/components/schemas/Bar' + - $ref: '#/components/schemas/Baz' + discriminator: + propertyName: code + mapping: + '1': '#/components/schemas/Bar' + '2': '#/components/schemas/Bar' + '3': '#/components/schemas/Baz' + Bar: + type: object + properties: + code: + type: integer + enum: + - 1 + - 2 + Baz: + type: object + properties: + code: + type: integer + enum: + - 3 diff --git a/specs/3.1.x/discriminator-mapped-many-number.yaml b/specs/3.1.x/discriminator-mapped-many-number.yaml new file mode 100644 index 0000000000..54fa8ae32e --- /dev/null +++ b/specs/3.1.x/discriminator-mapped-many-number.yaml @@ -0,0 +1,31 @@ +openapi: 3.1.0 +info: + title: OpenAPI 3.1.0 discriminator mapped many (number) example + version: 1 +components: + schemas: + Foo: + oneOf: + - $ref: '#/components/schemas/Bar' + - $ref: '#/components/schemas/Baz' + discriminator: + propertyName: code + mapping: + '1': '#/components/schemas/Bar' + '2': '#/components/schemas/Bar' + '3': '#/components/schemas/Baz' + Bar: + type: object + properties: + code: + type: integer + enum: + - 1 + - 2 + Baz: + type: object + properties: + code: + type: integer + enum: + - 3