From 8c45cbb82f704ab7dc456537c7aec4f4201a9e5d Mon Sep 17 00:00:00 2001 From: morgan-coded <256248948+morgan-coded@users.noreply.github.com> Date: Mon, 6 Jul 2026 21:08:12 -0500 Subject: [PATCH] fix(zod): support zod v4 mini schemas --- src/helpers/zod.ts | 41 +++++++++++++++++------- tests/helpers/zod.test.ts | 66 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 95 insertions(+), 12 deletions(-) diff --git a/src/helpers/zod.ts b/src/helpers/zod.ts index 2d9264507..53245f271 100644 --- a/src/helpers/zod.ts +++ b/src/helpers/zod.ts @@ -1,6 +1,7 @@ import { ResponseFormatJSONSchema } from '../resources/index'; import * as z3 from 'zod/v3'; import * as z4 from 'zod/v4'; +import * as z4Mini from 'zod/v4-mini'; import { AutoParseableResponseFormat, AutoParseableTextFormat, @@ -15,8 +16,12 @@ import { type ResponseFormatTextJSONSchemaConfig } from '../resources/responses/ import { toStrictJsonSchema } from '../lib/transform'; import { JSONSchema } from '../lib/jsonschema'; +type ZodV4Schema = z4.ZodType | z4Mini.ZodMiniType; +type ZodSchema = z3.ZodType | ZodV4Schema; + type InferZodType = - T extends z4.ZodType ? z4.infer + T extends z4Mini.ZodMiniType ? z4Mini.infer + : T extends z4.ZodType ? z4.infer : T extends z3.ZodType ? z3.infer : never; @@ -30,7 +35,7 @@ function zodV3ToJsonSchema(schema: z3.ZodType, options: { name: string }): Recor }); } -function zodV4ToJsonSchema(schema: z4.ZodType): Record { +function zodV4ToJsonSchema(schema: ZodV4Schema): Record { return toStrictJsonSchema( z4.toJSONSchema(schema, { target: 'draft-7', @@ -54,10 +59,24 @@ function zodV4ToJsonSchema(schema: z4.ZodType): Record { ) as Record; } -function isZodV4(zodObject: z3.ZodType | z4.ZodType): zodObject is z4.ZodType { +function isZodV4(zodObject: ZodSchema): zodObject is ZodV4Schema { return '_zod' in zodObject; } +function parseZodObject( + zodObject: ZodInput, + content: string, +): InferZodType { + const parsed = JSON.parse(content); + const parser = (zodObject as { parse?: (data: unknown) => unknown }).parse; + + if (typeof parser === 'function') { + return parser.call(zodObject, parsed) as InferZodType; + } + + return z4.parse(zodObject as ZodV4Schema, parsed) as InferZodType; +} + /** * Creates a chat completion `JSONSchema` response format object from * the given Zod schema. @@ -95,7 +114,7 @@ function isZodV4(zodObject: z3.ZodType | z4.ZodType): zodObject is z4.ZodType { * This can be passed directly to the `.create()` method but will not * result in any automatic parsing, you'll have to parse the response yourself. */ -export function zodResponseFormat( +export function zodResponseFormat( zodObject: ZodInput, name: string, props?: Omit, @@ -110,11 +129,11 @@ export function zodResponseFormat( schema: isZodV4(zodObject) ? zodV4ToJsonSchema(zodObject) : zodV3ToJsonSchema(zodObject, { name }), }, }, - (content) => zodObject.parse(JSON.parse(content)), + (content) => parseZodObject(zodObject, content), ); } -export function zodTextFormat( +export function zodTextFormat( zodObject: ZodInput, name: string, props?: Omit, @@ -127,7 +146,7 @@ export function zodTextFormat( strict: true, schema: isZodV4(zodObject) ? zodV4ToJsonSchema(zodObject) : zodV3ToJsonSchema(zodObject, { name }), }, - (content) => zodObject.parse(JSON.parse(content)), + (content) => parseZodObject(zodObject, content), ); } @@ -136,7 +155,7 @@ export function zodTextFormat( * automatically by the chat completion `.runTools()` method or automatically * parsed by `.parse()` / `.stream()`. */ -export function zodFunction(options: { +export function zodFunction(options: { name: string; parameters: Parameters; function?: ((args: InferZodType) => unknown | Promise) | undefined; @@ -162,12 +181,12 @@ export function zodFunction(options: }, { callback: options.function, - parser: (args) => options.parameters.parse(JSON.parse(args)), + parser: (args) => parseZodObject(options.parameters, args), }, ); } -export function zodResponsesFunction(options: { +export function zodResponsesFunction(options: { name: string; parameters: Parameters; function?: ((args: InferZodType) => unknown | Promise) | undefined; @@ -190,7 +209,7 @@ export function zodResponsesFunction }, { callback: options.function, - parser: (args) => options.parameters.parse(JSON.parse(args)), + parser: (args) => parseZodObject(options.parameters, args), }, ); } diff --git a/tests/helpers/zod.test.ts b/tests/helpers/zod.test.ts index 943ba9b7b..9e501f3bd 100644 --- a/tests/helpers/zod.test.ts +++ b/tests/helpers/zod.test.ts @@ -1,6 +1,8 @@ -import { zodResponseFormat, zodTextFormat } from 'openai/helpers/zod'; +import { zodFunction, zodResponseFormat, zodResponsesFunction, zodTextFormat } from 'openai/helpers/zod'; +import { expectType } from '../utils/typing'; import { z as zv3 } from 'zod/v3'; import { z as zv4 } from 'zod/v4'; +import { z as zv4Mini } from 'zod/v4-mini'; function collectRefs(value: unknown, refs: string[] = []): string[] { if (!value || typeof value !== 'object') return refs; @@ -57,6 +59,51 @@ it('converts Zod v4 discriminated unions to anyOf for strict schemas', () => { expect(schema.properties.data.anyOf).toHaveLength(2); }); +describe('Zod v4 mini', () => { + const MiniSchema = zv4Mini.object({ hello: zv4Mini.literal('world') }); + const expectedSchema = { + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + properties: { + hello: { + type: 'string', + const: 'world', + }, + }, + required: ['hello'], + additionalProperties: false, + }; + + it('supports response formats', () => { + const format = zodResponseFormat(MiniSchema, 'response'); + + expect(format.json_schema.schema).toEqual(expectedSchema); + expect(format.$parseRaw('{"hello":"world"}')).toEqual({ hello: 'world' }); + expect(() => format.$parseRaw('{"hello":"there"}')).toThrow(); + }); + + it('supports text formats', () => { + const format = zodTextFormat(MiniSchema, 'response'); + + expect(format.schema).toEqual(expectedSchema); + expect(format.$parseRaw('{"hello":"world"}')).toEqual({ hello: 'world' }); + expect(() => format.$parseRaw('{"hello":"there"}')).toThrow(); + }); + + it('supports tool argument parsing', () => { + const chatTool = zodFunction({ name: 'mini_tool', parameters: MiniSchema }); + const responseTool = zodResponsesFunction({ name: 'mini_tool', parameters: MiniSchema }); + + expect(chatTool.function.parameters).toEqual(expectedSchema); + expect(chatTool.$parseRaw('{"hello":"world"}')).toEqual({ hello: 'world' }); + expect(() => chatTool.$parseRaw('{"hello":"there"}')).toThrow(); + + expect(responseTool.parameters).toEqual(expectedSchema); + expect(responseTool.$parseRaw('{"hello":"world"}')).toEqual({ hello: 'world' }); + expect(() => responseTool.$parseRaw('{"hello":"there"}')).toThrow(); + }); +}); + describe.each([ { version: 'v3', z: zv3 }, { version: 'v4', z: zv4 as any as typeof zv3 }, @@ -560,3 +607,20 @@ describe.each([ }); } }); + +function _typeTests() { + const MiniSchema = zv4Mini.object({ hello: zv4Mini.literal('world') }); + + expectType<{ hello: 'world' }>(zodResponseFormat(MiniSchema, 'response').__output); + expectType<{ hello: 'world' }>(zodTextFormat(MiniSchema, 'response').__output); + zodFunction({ + name: 'mini_tool', + parameters: MiniSchema, + function: (args) => expectType<{ hello: 'world' }>(args), + }); + zodResponsesFunction({ + name: 'mini_tool', + parameters: MiniSchema, + function: (args) => expectType<{ hello: 'world' }>(args), + }); +}