fix(zod): convert oneOf to anyOf in toStrictJsonSchema for Zod v4 compatibility#1762
fix(zod): convert oneOf to anyOf in toStrictJsonSchema for Zod v4 compatibility#1762s-zx wants to merge 2 commits intoopenai:masterfrom
Conversation
…patibility Zod v4.1.13 changed discriminated unions to emit oneOf in JSON Schema output (colinhacks/zod#5453). The OpenAI strict-mode API does not accept oneOf (it returns HTTP 400 with 'oneOf is not permitted'), so any user on Zod v4.1.13+ who passes a discriminated union to zodResponseFormat / zodTextFormat / zodTool will get an unexpected API failure. toStrictJsonSchema() already handles anyOf by recursing into variants, but it had no handling for oneOf. Add a post-anyOf step that converts oneOf variants to anyOf in-place and deletes the original oneOf key. This mirrors the anyOf handling exactly, so each variant is still passed through ensureStrictJsonSchema. The conversion is semantically safe for the union types Zod generates because Zod oneOf variants are mutually exclusive object schemas; anyOf is the form the OpenAI API expects for structured-output unions. Fixes openai#1709
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e9183061c7
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // that Zod produces here, so we convert in-place and recurse into each variant. | ||
| const oneOf = (jsonSchema as any).oneOf; | ||
| if (Array.isArray(oneOf)) { | ||
| jsonSchema.anyOf = oneOf.map((variant: JSONSchemaDefinition, i: number) => |
There was a problem hiding this comment.
Preserve existing anyOf when converting oneOf
This assignment overwrites any pre-existing anyOf alternatives on the same schema node, so schemas that legitimately contain both anyOf and oneOf lose constraints during strictification. In that case, ensureStrictJsonSchema first normalizes anyOf, then clobbers it with converted oneOf variants, which changes validation behavior (accepting/rejecting different payloads than the original schema) instead of only translating unsupported keywords.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Done — merged oneOf into existing anyOf instead of overwriting. Schemas with both anyOf and oneOf now preserve all alternatives.
…g it
When ensureStrictJsonSchema encounters a schema node that already has an
anyOf array and also has a oneOf array, the previous code replaced anyOf
with the converted oneOf variants, silently dropping the anyOf constraints.
Spread both arrays so that all alternatives are preserved:
jsonSchema.anyOf = Array.isArray(jsonSchema.anyOf)
? [...jsonSchema.anyOf, ...convertedOneOf]
: convertedOneOf;
|
Good point — fixed. The previous code replaced jsonSchema.anyOf = Array.isArray(jsonSchema.anyOf)
? [...jsonSchema.anyOf, ...convertedOneOf]
: convertedOneOf;This preserves any pre-existing |
Problem
Zod v4.1.13 changed discriminated unions to emit
oneOfin JSON Schema output (colinhacks/zod#5453). The OpenAI strict-mode API does not acceptoneOf— it returns HTTP 400:Any user on Zod v4.1.13+ who passes a discriminated union to
zodResponseFormat/zodTextFormat/zodToolwill hit this failure.Reported in #1709.
Fix
After the existing
anyOfhandling block inensureStrictJsonSchema, add aoneOf→anyOfconversion step:Each variant still passes through
ensureStrictJsonSchema, so nested objects getadditionalProperties: falseand required fields are set correctly. The conversion is semantically safe: ZodoneOfvariants are mutually exclusive object schemas, andanyOfis the form the OpenAI API expects.Fixes #1709