From 9eee578fbb34b3ecfb5f822d439b81f48b92ed7d Mon Sep 17 00:00:00 2001 From: FU-max-boop Date: Fri, 29 May 2026 06:51:58 +0800 Subject: [PATCH 1/2] fix: convert zod oneOf unions to anyOf --- src/lib/transform.ts | 9 ++++++++- tests/helpers/zod.test.ts | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/lib/transform.ts b/src/lib/transform.ts index d241c215cb..f527888fd8 100644 --- a/src/lib/transform.ts +++ b/src/lib/transform.ts @@ -99,7 +99,14 @@ function ensureStrictJsonSchema( jsonSchema.items = ensureStrictJsonSchema(items, [...path, 'items'], root); } - // Handle unions (anyOf) + // Handle unions (anyOf). Zod v4 emits oneOf for discriminated unions, but + // OpenAI strict schemas only support anyOf. + const oneOf = jsonSchema.oneOf; + if (Array.isArray(oneOf)) { + jsonSchema.anyOf = [...(jsonSchema.anyOf ?? []), ...oneOf]; + delete jsonSchema.oneOf; + } + const anyOf = jsonSchema.anyOf; if (Array.isArray(anyOf)) { jsonSchema.anyOf = anyOf.map((variant, i) => diff --git a/tests/helpers/zod.test.ts b/tests/helpers/zod.test.ts index 1bc766376c..3524377082 100644 --- a/tests/helpers/zod.test.ts +++ b/tests/helpers/zod.test.ts @@ -143,6 +143,22 @@ describe.each([ `); }); + it('converts Zod discriminated unions to strict anyOf schemas', () => { + const ResponseSchema = z.object({ + data: z.discriminatedUnion('type', [ + z.object({ type: z.literal('a') }), + z.object({ type: z.literal('b') }), + ]), + }); + + const schema = zodResponseFormat(ResponseSchema, 'choice').json_schema.schema as Record; + const dataSchema = schema['properties']['data']; + + expect(dataSchema.oneOf).toBeUndefined(); + expect(dataSchema.anyOf).toHaveLength(2); + expect(JSON.stringify(schema)).not.toContain('"oneOf"'); + }); + it('allows description field to be passed in', () => { expect( zodResponseFormat( From 8882406ffc416b50df8dae706d58fd3d6fcfa092 Mon Sep 17 00:00:00 2001 From: FU-max-boop Date: Fri, 29 May 2026 12:11:02 +0800 Subject: [PATCH 2/2] Preserve anyOf constraints when converting oneOf --- src/lib/transform.ts | 9 ++++- tests/lib/transform.test.ts | 67 +++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/src/lib/transform.ts b/src/lib/transform.ts index f527888fd8..453a79459e 100644 --- a/src/lib/transform.ts +++ b/src/lib/transform.ts @@ -103,8 +103,15 @@ function ensureStrictJsonSchema( // OpenAI strict schemas only support anyOf. const oneOf = jsonSchema.oneOf; if (Array.isArray(oneOf)) { - jsonSchema.anyOf = [...(jsonSchema.anyOf ?? []), ...oneOf]; + const existingAnyOf = jsonSchema.anyOf; delete jsonSchema.oneOf; + + if (Array.isArray(existingAnyOf)) { + jsonSchema.allOf = [...(jsonSchema.allOf ?? []), { anyOf: existingAnyOf }, { anyOf: oneOf }]; + delete jsonSchema.anyOf; + } else { + jsonSchema.anyOf = oneOf; + } } const anyOf = jsonSchema.anyOf; diff --git a/tests/lib/transform.test.ts b/tests/lib/transform.test.ts index 87195d998d..93cc3bd842 100644 --- a/tests/lib/transform.test.ts +++ b/tests/lib/transform.test.ts @@ -271,6 +271,73 @@ describe('toStrictJsonSchema', () => { } `); }); + + test('keeps existing anyOf separate when converting oneOf', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + value: { + anyOf: [ + { + type: 'object', + properties: { str: { type: 'string' } }, + required: ['str'], + }, + ], + oneOf: [ + { + type: 'object', + properties: { num: { type: 'number' } }, + required: ['num'], + }, + ], + }, + }, + required: ['value'], + }; + + const strict = toStrictJsonSchema(schema); + const valueSchema = strict.properties?.['value'] as JSONSchema; + + expect(valueSchema.oneOf).toBeUndefined(); + expect(valueSchema.anyOf).toBeUndefined(); + expect(valueSchema.allOf).toMatchInlineSnapshot(` + [ + { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "str": { + "type": "string", + }, + }, + "required": [ + "str", + ], + "type": "object", + }, + ], + }, + { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "num": { + "type": "number", + }, + }, + "required": [ + "num", + ], + "type": "object", + }, + ], + }, + ] + `); + }); }); describe('allOf Handling', () => {