Skip to content

fix(zod): fall back to z.union for discriminated unions with empty-object members#3915

Open
pullfrog[bot] wants to merge 1 commit into
mainfrom
pullfrog/3914-zod-discriminated-union-empty-object
Open

fix(zod): fall back to z.union for discriminated unions with empty-object members#3915
pullfrog[bot] wants to merge 1 commit into
mainfrom
pullfrog/3914-zod-discriminated-union-empty-object

Conversation

@pullfrog
Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot commented May 19, 2026

Closes #3914.

Problem

When an OpenAPI oneOf/anyOf with a discriminator referenced an empty object schema (type: object with no properties), the generated Zod schema failed to type-check:

export const zEmpty = z.record(z.string(), z.unknown());

export const zTestResponse = z.discriminatedUnion('type', [
    zEmpty.extend({ type: z.literal('Empty') }), // ❌ `.extend` does not exist on z.record
    zWithValue.extend({ type: z.literal('WithValue') })
]);

zEmpty is rendered as z.record(...) (because the empty object has no properties but the parser injects additionalProperties: { type: 'unknown' }), and z.record(...) has no .extend(...) method.

Fix

tryBuildDiscriminatedUnion in packages/openapi-ts/src/plugins/zod/shared/discriminated-union.ts now resolves each $ref member and returns null when the resolved schema is record-shaped (type: 'object', no properties, additionalProperties set). This delegates to the existing z.union(...) fallback path, which renders compileable z.object({ type: z.literal(...) }).and(zEmpty) members for v3/v4 and z.intersection(...) for mini.

export const zTestResponse = z.union([
    z.object({
        type: z.literal('Empty')
    }).and(zEmpty),
    z.object({
        type: z.literal('WithValue')
    }).and(zWithValue)
]);

Tests

Added discriminator-empty-object-member scenario to all four zod test files (v3/v4 × 3.0.x/3.1.x) with new specs/{3.0.x,3.1.x}/discriminator-empty-object-member.yaml inputs and 12 generated snapshots (3 dialects × 2 OAS versions × 2 zod packages).

  • pnpm vitest run --project @test/openapi-ts-zod-v3 --project @test/openapi-ts-zod-v4 — 210 passed
  • pnpm ty @hey-api/openapi-ts — clean
  • pnpm lint — no new warnings

Pullfrog  | View workflow run | via Pullfrog | Using Claude Opus𝕏

…ject members

When an OpenAPI oneOf/anyOf discriminator referenced an empty object schema (no properties), the Zod codegen emitted that schema as `z.record(...)` and then attempted to call `.extend({ discriminator: z.literal(value) })` on it, which does not type-check. Detect record-shaped refs in `tryBuildDiscriminatedUnion` and return null so the existing `z.union(...)` fallback path renders `z.object({...}).and(zEmpty)`-style members that compile.

Closes #3914
@pullfrog pullfrog Bot requested a review from mrlubos May 19, 2026 14:29
@bolt-new-by-stackblitz
Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label May 19, 2026
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 19, 2026

🦋 Changeset detected

Latest commit: 8fa9f3a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@hey-api/openapi-ts Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown

vercel Bot commented May 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
web Ready Ready Preview, Comment May 19, 2026 2:30pm

Request Review

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 19, 2026

Open in StackBlitz

@hey-api/codegen-core

npm i https://pkg.pr.new/@hey-api/codegen-core@3915

@hey-api/json-schema-ref-parser

npm i https://pkg.pr.new/@hey-api/json-schema-ref-parser@3915

@hey-api/nuxt

npm i https://pkg.pr.new/@hey-api/nuxt@3915

@hey-api/openapi-ts

npm i https://pkg.pr.new/@hey-api/openapi-ts@3915

@hey-api/shared

npm i https://pkg.pr.new/@hey-api/shared@3915

@hey-api/spec-types

npm i https://pkg.pr.new/@hey-api/spec-types@3915

@hey-api/types

npm i https://pkg.pr.new/@hey-api/types@3915

@hey-api/vite-plugin

npm i https://pkg.pr.new/@hey-api/vite-plugin@3915

commit: 8fa9f3a

@codecov
Copy link
Copy Markdown

codecov Bot commented May 19, 2026

Codecov Report

❌ Patch coverage is 0% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 37.69%. Comparing base (0c865e1) to head (8fa9f3a).

Files with missing lines Patch % Lines
...i-ts/src/plugins/zod/shared/discriminated-union.ts 0.00% 3 Missing and 4 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3915      +/-   ##
==========================================
- Coverage   37.70%   37.69%   -0.02%     
==========================================
  Files         582      582              
  Lines       20844    20851       +7     
  Branches     6063     6067       +4     
==========================================
  Hits         7860     7860              
- Misses      10570    10573       +3     
- Partials     2414     2418       +4     
Flag Coverage Δ
unittests 37.69% <0.00%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug 🔥 Broken or incorrect behavior. size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Invalid zod schema generated if one alternative is an empty object

0 participants