Description
Package/version: @hey-api/openapi-ts@0.98.2 · plugins: @hey-api/typescript, zod · zod 4
Summary
When a oneOf + discriminator schema has variants defined via allOf (a shared
base object + a per-variant object), the zod plugin renders each variant as
zBase.and(z.object({ … })) — a ZodIntersection. Because a ZodIntersection is
not a ZodObject, the plugin can't use it as a z.discriminatedUnion member, so it
falls back to z.union([...]) for the entire envelope. A single allOf variant
degrades all of them, including the ones written as flat objects.
This is the allOf case originally shown in #3270. That issue was closed via #3780,
but the shipped fix only covers variants that are directly object schemas; the
allOf-composed variant still regresses on 0.98.2. So "issue closed" doesn't cover
this path — hence a fresh report.
Minimal reproduction
Single-file spec:
openapi: 3.1.0
info: { title: repro, version: 1.0.0 }
paths: {}
components:
schemas:
Base:
type: object
required: [kind, id]
properties:
kind: { type: string }
id: { type: string }
Cat:
allOf:
- $ref: '#/components/schemas/Base'
- type: object
required: [meowVolume]
properties:
kind: { type: string, enum: [cat] }
meowVolume: { type: integer }
Dog:
allOf:
- $ref: '#/components/schemas/Base'
- type: object
required: [barkVolume]
properties:
kind: { type: string, enum: [dog] }
barkVolume: { type: integer }
Animal:
oneOf:
- $ref: '#/components/schemas/Cat'
- $ref: '#/components/schemas/Dog'
discriminator:
propertyName: kind
mapping:
cat: '#/components/schemas/Cat'
dog: '#/components/schemas/Dog'
Config: plugins: ['@hey-api/typescript', 'zod'].
Actual output (zod)
export const zCat = zBase.and(z.object({ kind: z.enum(['cat']), meowVolume: z.int() }));
export const zDog = zBase.and(z.object({ kind: z.enum(['dog']), barkVolume: z.int() }));
export const zAnimal = z.union([
z.object({ kind: z.literal('cat') }).and(zCat),
z.object({ kind: z.literal('dog') }).and(zDog),
]);
Expected output (zod)
Flatten the allOf object members into a single ZodObject before assembling the
union — i.e. the same output produced when the variants are written as flat objects:
export const zCat = z.object({ id: z.string(), kind: z.enum(['cat']), meowVolume: z.int() });
export const zDog = z.object({ id: z.string(), kind: z.enum(['dog']), barkVolume: z.int() });
export const zAnimal = z.discriminatedUnion('kind', [
zCat.extend({ kind: z.literal('cat') }),
zDog.extend({ kind: z.literal('dog') }),
]);
Impact
- Loses
z.discriminatedUnion narrowing and its targeted parse-error messages.
- Breaks downstream code that walks
schema.options[i].shape — union members are
ZodIntersection, which has no .shape.
- Forces authors to inline the shared envelope fields into every variant (no
allOf
reuse), which is painful at scale (hundreds of variants).
Reproducible example or configuration
No response
OpenAPI specification (optional)
No response
System information (optional)
No response
Description
Package/version:
@hey-api/openapi-ts@0.98.2· plugins:@hey-api/typescript,zod· zod 4Summary
When a
oneOf+discriminatorschema has variants defined viaallOf(a sharedbase object + a per-variant object), the zod plugin renders each variant as
zBase.and(z.object({ … }))— aZodIntersection. Because aZodIntersectionisnot a
ZodObject, the plugin can't use it as az.discriminatedUnionmember, so itfalls back to
z.union([...])for the entire envelope. A singleallOfvariantdegrades all of them, including the ones written as flat objects.
This is the
allOfcase originally shown in #3270. That issue was closed via #3780,but the shipped fix only covers variants that are directly object schemas; the
allOf-composed variant still regresses on 0.98.2. So "issue closed" doesn't coverthis path — hence a fresh report.
Minimal reproduction
Single-file spec:
Config:
plugins: ['@hey-api/typescript', 'zod'].Actual output (zod)
Expected output (zod)
Flatten the
allOfobject members into a singleZodObjectbefore assembling theunion — i.e. the same output produced when the variants are written as flat objects:
Impact
z.discriminatedUnionnarrowing and its targeted parse-error messages.schema.options[i].shape— union members areZodIntersection, which has no.shape.allOfreuse), which is painful at scale (hundreds of variants).
Reproducible example or configuration
No response
OpenAPI specification (optional)
No response
System information (optional)
No response