diff --git a/src/helpers/zod.ts b/src/helpers/zod.ts index 400b448fdd..2d92645078 100644 --- a/src/helpers/zod.ts +++ b/src/helpers/zod.ts @@ -34,6 +34,22 @@ function zodV4ToJsonSchema(schema: z4.ZodType): Record { return toStrictJsonSchema( z4.toJSONSchema(schema, { target: 'draft-7', + override: ({ zodSchema, jsonSchema }) => { + const def = zodSchema._zod.def; + + if (def.type === 'union' && 'discriminator' in def && Array.isArray(jsonSchema.oneOf)) { + if (jsonSchema.anyOf !== undefined) { + throw new Error( + 'Zod discriminated union generated both `anyOf` and `oneOf`, which cannot be represented in an OpenAI strict schema', + ); + } + + // Discriminator values are mutually exclusive, so anyOf preserves the + // union while staying inside the API's supported JSON Schema subset. + jsonSchema.anyOf = jsonSchema.oneOf; + delete jsonSchema.oneOf; + } + }, }) as JSONSchema, ) as Record; } diff --git a/tests/helpers/zod.test.ts b/tests/helpers/zod.test.ts index 2b6f21f561..04627b88c1 100644 --- a/tests/helpers/zod.test.ts +++ b/tests/helpers/zod.test.ts @@ -30,6 +30,20 @@ function expectDefinitionRefsToResolve(schema: Record) { visit(schema, new Set()); } +it('converts Zod v4 discriminated unions to anyOf for strict schemas', () => { + const ResponseSchema = zv4.object({ + data: zv4.discriminatedUnion('type', [ + zv4.object({ type: zv4.literal('a') }), + zv4.object({ type: zv4.literal('b') }), + ]), + }); + + const schema = zodResponseFormat(ResponseSchema, 'choice').json_schema.schema as any; + + expect(JSON.stringify(schema)).not.toContain('"oneOf"'); + expect(schema.properties.data.anyOf).toHaveLength(2); +}); + describe.each([ { version: 'v3', z: zv3 }, { version: 'v4', z: zv4 as any as typeof zv3 },