feat(v4): add union option to z.toJSONSchema for oneOf opt-in (#5807, #5493, #5494)#5930
feat(v4): add union option to z.toJSONSchema for oneOf opt-in (#5807, #5493, #5494)#5930dokson wants to merge 1 commit into
union option to z.toJSONSchema for oneOf opt-in (#5807, #5493, #5494)#5930Conversation
…olinhacks#5807, colinhacks#5493, colinhacks#5494) Multiple consumers of `z.toJSONSchema` reject the `anyOf` keyword that v4 emits for inclusive unions: - Google Gemini Function Calling (colinhacks#5807) — schemas containing `anyOf` cause silent failures or validation errors. - Swagger codegen tooling (colinhacks#5494) — generates broken bindings for `anyOf` shapes. - Older OpenAPI / language-binding generators (colinhacks#5493) — same shape. The default is sound and stays put — `z.union(...)` is non-exclusive by spec, so `anyOf` mirrors validation semantics (colinhacks#5494 (comment)). Add an opt-in `union: "anyOf" | "oneOf"` parameter so users with strict consumers can flip the encoding without resorting to a manual `override` walk that intercepts every `anyOf` node: ```ts z.toJSONSchema(schema, { union: "oneOf" }); ``` Discriminated unions and `z.xor()` are unaffected — they always emit `oneOf` because they are exclusive by construction. Implementation: - New `union` field on `JSONSchemaGeneratorParams` and `ToJSONSchemaContext`, defaulted to `"anyOf"` in `initializeContext`. - `unionProcessor` reads `ctx.union` and chooses the keyword once, then writes the options under that key. 5 tests cover: default `anyOf` behavior, explicit `"anyOf"`, opt-in `"oneOf"`, discriminated-union invariance, nested unions inside objects.
|
TL;DR — Adds a Key changes
Summary | 3 files | 1 commit | base:
|
There was a problem hiding this comment.
Reviewed — no issues found.
Claude Opus | 𝕏
There was a problem hiding this comment.
Reviewed — no issues found.
Claude Opus | 𝕏
|
Very allergic to additional params on this function. I'm considering a plugin API that would give Zod the ability to encapsulate some of these common overrides and provides them to users via a consumable API. |

Closes #5807. Also addresses #5493 and #5494 which surface the same need from a different consumer.
The problem
Multiple downstream consumers of
z.toJSONSchema()reject theanyOfkeyword that v4 emits for inclusive unions:anyOfcause silent tool-call failures or validation errors.anyOfshapes.The default is sound, and stays put. As you noted in #5494 (comment):
z.union(...)is inclusive — multiple branches can match the same value — soanyOfmatches the spec semantics. This PR doesn't change that.What this adds
A new opt-in parameter on
z.toJSONSchema():When set, inclusive unions emit
oneOfinstead ofanyOf. Default is"anyOf".Discriminated unions and
z.xor()are unaffected — they're exclusive by construction and keep emittingoneOfregardless.Why a flag and not just
overrideUsers hit by this today work around it by walking the produced schema after the fact:
It works but it's brittle: it rewrites the path keys after they've been computed (the
patharg in any useroverridealready referencesanyOfindices), it has to be duplicated in every project that hits the issue, and it's not discoverable from the docs. A flag is one parameter, doesn't cost anything when unused, and the path is correct from the start.Implementation
JSONSchemaGeneratorParamsandToJSONSchemaContextget aunion: "anyOf" | "oneOf"field.initializeContextdefaults it to"anyOf".unionProcessorpicks the keyword once:oneOfif exclusive or opted in, elseanyOf. The path argument and the output key are both written under the chosen keyword.Test coverage
5 tests in
tests/to-json-schema-union.test.ts:anyOffor inclusive union.union: "oneOf"emitsoneOffor inclusive union.union: "anyOf"matches the default.oneOfregardless of the flag.pnpm vitest run to-json-schema*— 344/344 passing locally with 0 type errors. No existing snapshots changed (default unchanged).