-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
fix: use oneOf for discriminated unions in JSON Schema #5453
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: use oneOf for discriminated unions in JSON Schema #5453
Conversation
WalkthroughThe changes implement support for discriminated unions in JSON Schema generation for Zod v4. When generating schemas, discriminated unions now emit oneOf instead of anyOf when a discriminator is detected via Pre-merge checks✅ Passed checks (3 passed)
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. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
packages/zod/src/v4/classic/tests/to-json-schema.test.ts (1)
660-707: Nice test coverage for the basic case!The test validates that discriminated unions correctly generate
oneOfwith proper structure, including theconstvalues for the discriminator field. The inline snapshot approach is solid for catching any regressions.Consider adding a few more test cases to cover edge scenarios:
- Different discriminator keys (not just "type")
- More than 2 union variants
- Single variant edge case
- Different targets (draft-7, openapi-3.0) to ensure oneOf works across specs
These are nice-to-haves for more comprehensive coverage, but the current test is good for the main use case.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
packages/zod/src/v4/classic/tests/to-json-schema.test.ts(1 hunks)packages/zod/src/v4/core/to-json-schema.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (12)
**/*.{js,jsx,ts,tsx,mjs,cjs,json}
📄 CodeRabbit inference engine (CLAUDE.md)
Enforce line width of 120 characters via Biome formatting
Files:
packages/zod/src/v4/core/to-json-schema.tspackages/zod/src/v4/classic/tests/to-json-schema.test.ts
**/*.{js,jsx,ts,tsx,mjs,cjs}
📄 CodeRabbit inference engine (CLAUDE.md)
Use ES5-style trailing commas in JavaScript/TypeScript code
Files:
packages/zod/src/v4/core/to-json-schema.tspackages/zod/src/v4/classic/tests/to-json-schema.test.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Allow the any type in TypeScript (noExplicitAny off)
Allow non-null assertions in TypeScript (noNonNullAssertion off)
Write TypeScript to pass strict mode with exactOptionalPropertyTypes enabled
Use NodeNext module resolution semantics for imports in TypeScript
Target ES2020 language features in TypeScript source
Files:
packages/zod/src/v4/core/to-json-schema.tspackages/zod/src/v4/classic/tests/to-json-schema.test.ts
**/*.{ts,tsx,js,jsx,mjs,cjs}
📄 CodeRabbit inference engine (CLAUDE.md)
Allow parameter reassignment for performance-sensitive code (noParameterAssign off)
Files:
packages/zod/src/v4/core/to-json-schema.tspackages/zod/src/v4/classic/tests/to-json-schema.test.ts
**/*.{js,mjs,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/development-setup.mdc)
**/*.{js,mjs,ts,tsx}: Use .js extensions in import specifiers (e.g., import { z } from "./index.js")
Don’t use require(); use ESM import statements
Files:
packages/zod/src/v4/core/to-json-schema.tspackages/zod/src/v4/classic/tests/to-json-schema.test.ts
**/*.{js,mjs,cjs,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/guidelines.mdc)
Do not leave log statements (e.g., console.log, debugger) in tests or production code
Files:
packages/zod/src/v4/core/to-json-schema.tspackages/zod/src/v4/classic/tests/to-json-schema.test.ts
packages/**/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/zod-project-guide.mdc)
Write source code in TypeScript (TypeScript-first codebase)
Files:
packages/zod/src/v4/core/to-json-schema.tspackages/zod/src/v4/classic/tests/to-json-schema.test.ts
packages/zod/**
📄 CodeRabbit inference engine (.cursor/rules/zod-project-guide.mdc)
Make core Zod library changes in the main package at packages/zod/
Files:
packages/zod/src/v4/core/to-json-schema.tspackages/zod/src/v4/classic/tests/to-json-schema.test.ts
packages/zod/src/{v4/classic/tests,v4/core/tests,v3/tests}/**/*
📄 CodeRabbit inference engine (.cursor/rules/testing-guidelines.mdc)
Place all test files under packages/zod/src/v4/classic/tests, packages/zod/src/v4/core/tests, or packages/zod/src/v3/tests
Files:
packages/zod/src/v4/classic/tests/to-json-schema.test.ts
packages/zod/src/{v4/classic/tests,v4/core/tests,v3/tests}/**/*.test.ts
📄 CodeRabbit inference engine (.cursor/rules/testing-guidelines.mdc)
packages/zod/src/{v4/classic/tests,v4/core/tests,v3/tests}/**/*.test.ts: Test files must use the .test.ts extension (TypeScript), not JavaScript
Use import type for type-only imports in tests (e.g.,import type { ... })
Permanent, regression, API validation, edge-case coverage, and performance benchmark tests must be in the test suite (not play.ts)
Use Vitest as the framework in tests and import from it asimport { expect, test } from "vitest"
Import Zod in tests asimport * as z from "zod/v4"
Write tests with clear, descriptive names and cover both success and failure cases
Keep test suites concise while maintaining adequate coverage
Do not skip tests due to type issues; fix the types instead
Use descriptive file names like string.test.ts, object.test.ts, url-validation.test.ts and group related functionality together
Files:
packages/zod/src/v4/classic/tests/to-json-schema.test.ts
packages/zod/src/v4/{classic,core}/tests/**/*.test.ts
📄 CodeRabbit inference engine (.cursor/rules/testing-workflow.mdc)
packages/zod/src/v4/{classic,core}/tests/**/*.test.ts: Use Vitest for all testing (Vitest APIs in test files)
Place tests under packages/zod/src/v4/classic/tests/ or packages/zod/src/v4/core/tests/
Name test files with the *.test.ts suffix
Use test() for individual test cases
Use describe() for test groups
Use expect() for assertions
Files:
packages/zod/src/v4/classic/tests/to-json-schema.test.ts
packages/**/*.test.ts
📄 CodeRabbit inference engine (.cursor/rules/zod-project-guide.mdc)
Use Vitest for tests and place test cases in .test.ts files
Files:
packages/zod/src/v4/classic/tests/to-json-schema.test.ts
🧠 Learnings (18)
📓 Common learnings
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/schemas.ts : Ensure readonly wrapper types (e.g., $ZodReadonly) pass through values for discriminator support in unions
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/schemas.ts : For $ZodDiscriminatedUnion, compute and merge propValues lazily from options; ensure each option provides the discriminator key and that values are unique
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/schemas.ts : Wrapper schemas (e.g., $ZodOptional, $ZodNullable, $ZodReadonly) must pass through internal properties from their inner type using util.defineLazy for propValues, values, optin, and optout
📚 Learning: 2025-10-21T17:27:32.492Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/schemas.ts : Ensure readonly wrapper types (e.g., $ZodReadonly) pass through values for discriminator support in unions
Applied to files:
packages/zod/src/v4/core/to-json-schema.tspackages/zod/src/v4/classic/tests/to-json-schema.test.ts
📚 Learning: 2025-10-21T17:27:32.492Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/schemas.ts : For $ZodDiscriminatedUnion, compute and merge propValues lazily from options; ensure each option provides the discriminator key and that values are unique
Applied to files:
packages/zod/src/v4/core/to-json-schema.tspackages/zod/src/v4/classic/tests/to-json-schema.test.ts
📚 Learning: 2025-10-21T17:27:32.492Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/schemas.ts : Wrapper schemas (e.g., $ZodOptional, $ZodNullable, $ZodReadonly) must pass through internal properties from their inner type using util.defineLazy for propValues, values, optin, and optout
Applied to files:
packages/zod/src/v4/core/to-json-schema.tspackages/zod/src/v4/classic/tests/to-json-schema.test.ts
📚 Learning: 2025-10-21T17:27:32.492Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/{schemas.ts,core.ts} : Use the custom constructor system via core.$constructor() and initialize instances with $ZodType.init() when creating schemas
Applied to files:
packages/zod/src/v4/core/to-json-schema.ts
📚 Learning: 2025-10-21T17:27:32.492Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/schemas.ts : Implement schema parse functions following the standard structure: type check, push invalid_type issue on mismatch, optionally coerce/transform, and return payload
Applied to files:
packages/zod/src/v4/core/to-json-schema.ts
📚 Learning: 2025-10-21T17:26:08.288Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/testing-guidelines.mdc:0-0
Timestamp: 2025-10-21T17:26:08.288Z
Learning: Applies to packages/zod/src/{v4/classic/tests,v4/core/tests,v3/tests}/**/*.test.ts : Use import type for type-only imports in tests (e.g., `import type { ... }`)
Applied to files:
packages/zod/src/v4/core/to-json-schema.tspackages/zod/src/v4/classic/tests/to-json-schema.test.ts
📚 Learning: 2025-10-21T17:26:08.288Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/testing-guidelines.mdc:0-0
Timestamp: 2025-10-21T17:26:08.288Z
Learning: Applies to packages/zod/src/{v4/classic/tests,v4/core/tests,v3/tests}/**/*.test.ts : Do not skip tests due to type issues; fix the types instead
Applied to files:
packages/zod/src/v4/core/to-json-schema.tspackages/zod/src/v4/classic/tests/to-json-schema.test.ts
📚 Learning: 2025-10-21T17:27:32.492Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/errors.ts : Define and use canonical error types/codes as declared in errors.ts (e.g., invalid_type, TooBig/TooSmall, InvalidStringFormat, InvalidUnion, Custom)
Applied to files:
packages/zod/src/v4/core/to-json-schema.ts
📚 Learning: 2025-10-21T17:26:08.288Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/testing-guidelines.mdc:0-0
Timestamp: 2025-10-21T17:26:08.288Z
Learning: Applies to packages/zod/src/{v4/classic/tests,v4/core/tests,v3/tests}/**/*.test.ts : Import Zod in tests as `import * as z from "zod/v4"`
Applied to files:
packages/zod/src/v4/core/to-json-schema.tspackages/zod/src/v4/classic/tests/to-json-schema.test.ts
📚 Learning: 2025-10-21T17:27:32.492Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.492Z
Learning: Applies to packages/zod/src/v4/core/schemas.ts : Define computed internal properties using util.defineLazy() to avoid circular dependencies
Applied to files:
packages/zod/src/v4/core/to-json-schema.ts
📚 Learning: 2025-10-21T17:26:08.288Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/testing-guidelines.mdc:0-0
Timestamp: 2025-10-21T17:26:08.288Z
Learning: Applies to packages/zod/src/{v4/classic/tests,v4/core/tests,v3/tests}/**/*.test.ts : Keep test suites concise while maintaining adequate coverage
Applied to files:
packages/zod/src/v4/classic/tests/to-json-schema.test.ts
📚 Learning: 2025-10-21T17:26:08.288Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/testing-guidelines.mdc:0-0
Timestamp: 2025-10-21T17:26:08.288Z
Learning: Applies to packages/zod/src/{v4/classic/tests,v4/core/tests,v3/tests}/**/*.test.ts : Permanent, regression, API validation, edge-case coverage, and performance benchmark tests must be in the test suite (not play.ts)
Applied to files:
packages/zod/src/v4/classic/tests/to-json-schema.test.ts
📚 Learning: 2025-10-21T17:26:32.924Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/testing-workflow.mdc:0-0
Timestamp: 2025-10-21T17:26:32.924Z
Learning: Applies to packages/zod/src/v4/{classic,core}/tests/**/*.test.ts : Place tests under packages/zod/src/v4/classic/tests/ or packages/zod/src/v4/core/tests/
Applied to files:
packages/zod/src/v4/classic/tests/to-json-schema.test.ts
📚 Learning: 2025-10-21T17:26:08.288Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/testing-guidelines.mdc:0-0
Timestamp: 2025-10-21T17:26:08.288Z
Learning: Applies to packages/zod/src/{v4/classic/tests,v4/core/tests,v3/tests}/**/*.test.ts : Use descriptive file names like string.test.ts, object.test.ts, url-validation.test.ts and group related functionality together
Applied to files:
packages/zod/src/v4/classic/tests/to-json-schema.test.ts
📚 Learning: 2025-10-21T17:26:32.924Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/testing-workflow.mdc:0-0
Timestamp: 2025-10-21T17:26:32.924Z
Learning: Applies to packages/zod/src/v4/{classic,core}/tests/**/*.test.ts : Use test() for individual test cases
Applied to files:
packages/zod/src/v4/classic/tests/to-json-schema.test.ts
📚 Learning: 2025-10-21T17:26:08.288Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/testing-guidelines.mdc:0-0
Timestamp: 2025-10-21T17:26:08.288Z
Learning: Applies to packages/zod/src/{v4/classic/tests,v4/core/tests,v3/tests}/**/*.test.ts : Write tests with clear, descriptive names and cover both success and failure cases
Applied to files:
packages/zod/src/v4/classic/tests/to-json-schema.test.ts
📚 Learning: 2025-10-21T17:26:32.924Z
Learnt from: CR
Repo: colinhacks/zod PR: 0
File: .cursor/rules/testing-workflow.mdc:0-0
Timestamp: 2025-10-21T17:26:32.924Z
Learning: Applies to packages/zod/src/v4/{classic,core}/tests/**/*.test.ts : Use expect() for assertions
Applied to files:
packages/zod/src/v4/classic/tests/to-json-schema.test.ts
🧬 Code graph analysis (1)
packages/zod/src/v4/core/to-json-schema.ts (3)
packages/zod/src/v3/types.ts (3)
options(3043-3045)options(3166-3168)options(4112-4114)packages/zod/src/v4/classic/schemas.ts (1)
json(2182-2188)packages/zod/src/v4/mini/schemas.ts (1)
json(1655-1660)
🔇 Additional comments (1)
packages/zod/src/v4/core/to-json-schema.ts (1)
333-346: Need to verify the type ofdefin the union case. Let me check the context into-json-schema.ts:Perfect! Issue #4873 directly addresses this: the discriminator key is incorrectly typed as$ZodUnionDefinstead of$ZodDiscriminatedUnionDef. This confirms that the type cast in the code under review is indeed a known workaround for a type system limitation.Based on the verification:
- The first script output confirmed that
$ZodDiscriminatedUnionDefinterface properly definesdiscriminator: Discon line 2119- The second script showed that the code properly uses
def.discriminatorwithout issues in the runtime at lines 2164, 2190, and 2205- The web search revealed that the type mismatch is actually a known issue (#4873) where the TypeScript type system doesn't properly reflect that discriminated union defs have the discriminator property
- The logic for routing to
oneOfvsanyOfmatches JSON Schema specifications and aligns with Zod's designAll my learnings confirm the logic is sound. The code handles discriminator access correctly, even though the type system requires a workaround.
The discriminator property check is correct—discriminated unions are guaranteed to have this property.
The logic properly distinguishes between regular unions (using
anyOf) and discriminated unions (usingoneOf), which aligns with JSON Schema semantics. The type cast(def as any)is a necessary workaround for a TypeScript type mismatch that's documented in Zod issue #4873, but the runtime behavior is solid.
pawk3k
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good
|
Great call, love this. |
|
Hey team, it seems this didn't land on |
Description
Fixes #4089 - discriminated unions generating
anyOfinstead ofoneOfin JSON Schema output.Discriminated unions should use
oneOf(exactly one match) instead ofanyOf(one or more matches) because the discriminator field ensures mutual exclusivity between options. This is semantically correct according to JSON Schema specifications.Changes
to-json-schema.tsto detect discriminated unions via thediscriminatorpropertyoneOfin JSON SchemaanyOfas beforeto-json-schema.test.tsTesting
✅ All existing tests pass
✅ Added new test case for discriminated unions
✅ Test validates correct oneOf generation with proper schema structure