From 8f56474143e6d6d25debb8737e2534de8d8019e3 Mon Sep 17 00:00:00 2001 From: karpetrosyan Date: Wed, 1 Oct 2025 09:25:40 +0400 Subject: [PATCH 1/9] feat: add support for zod@4 schemas --- src/helpers/zod.ts | 41 ++- src/lib/jsonschema.ts | 24 ++ src/lib/transform.ts | 165 +++++++++++ tests/lib/transform.test.ts | 535 ++++++++++++++++++++++++++++++++++++ 4 files changed, 761 insertions(+), 4 deletions(-) create mode 100644 src/lib/transform.ts create mode 100644 tests/lib/transform.test.ts diff --git a/src/helpers/zod.ts b/src/helpers/zod.ts index 6495cc99d..4f0e8bd24 100644 --- a/src/helpers/zod.ts +++ b/src/helpers/zod.ts @@ -1,5 +1,6 @@ import { ResponseFormatJSONSchema } from '../resources/index'; import type { infer as zodInfer, ZodType } from 'zod/v3'; +import { toJSONSchema, type infer as zodInferV4, type ZodType as ZodTypeV4 } from 'zod/v4'; import { AutoParseableResponseFormat, AutoParseableTextFormat, @@ -11,6 +12,8 @@ import { import { zodToJsonSchema as _zodToJsonSchema } from '../_vendor/zod-to-json-schema'; import { AutoParseableResponseTool, makeParseableResponseTool } from '../lib/ResponsesParser'; import { type ResponseFormatTextJSONSchemaConfig } from '../resources/responses/responses'; +import { toStrictJsonSchema } from '../lib/transform'; +import { JSONSchema } from '../lib/jsonschema'; function zodToJsonSchema(schema: ZodType, options: { name: string }): Record { return _zodToJsonSchema(schema, { @@ -22,6 +25,10 @@ function zodToJsonSchema(schema: ZodType, options: { name: string }): Record( zodObject: ZodInput, name: string, props?: Omit, -): AutoParseableResponseFormat> { +): AutoParseableResponseFormat>; +export function zodResponseFormat( + zodObject: ZodInput, + name: string, + props?: Omit, +): AutoParseableResponseFormat>; +export function zodResponseFormat( + zodObject: ZodInput, + name: string, + props?: Omit, +): unknown { return makeParseableResponseFormat( { type: 'json_schema', @@ -71,7 +88,10 @@ export function zodResponseFormat( ...props, name, strict: true, - schema: zodToJsonSchema(zodObject, { name }), + schema: + isZodV4(zodObject) ? + (toStrictJsonSchema(toJSONSchema(zodObject) as JSONSchema) as Record) + : zodToJsonSchema(zodObject, { name }), }, }, (content) => zodObject.parse(JSON.parse(content)), @@ -82,14 +102,27 @@ export function zodTextFormat( zodObject: ZodInput, name: string, props?: Omit, -): AutoParseableTextFormat> { +): AutoParseableTextFormat>; +export function zodTextFormat( + zodObject: ZodInput, + name: string, + props?: Omit, +): AutoParseableTextFormat>; +export function zodTextFormat( + zodObject: ZodInput, + name: string, + props?: Omit, +): unknown { return makeParseableTextFormat( { type: 'json_schema', ...props, name, strict: true, - schema: zodToJsonSchema(zodObject, { name }), + schema: + isZodV4(zodObject) ? + (toStrictJsonSchema(toJSONSchema(zodObject) as JSONSchema) as Record) + : zodToJsonSchema(zodObject, { name }), }, (content) => zodObject.parse(JSON.parse(content)), ); diff --git a/src/lib/jsonschema.ts b/src/lib/jsonschema.ts index 636277705..45eda0527 100644 --- a/src/lib/jsonschema.ts +++ b/src/lib/jsonschema.ts @@ -131,6 +131,30 @@ export interface JSONSchema { oneOf?: JSONSchemaDefinition[] | undefined; not?: JSONSchemaDefinition | undefined; + /** + * @see https://json-schema.org/draft/2020-12/json-schema-core.html#section-8.2.4 + */ + $defs?: + | { + [key: string]: JSONSchemaDefinition; + } + | undefined; + + /** + * @deprecated Use $defs instead (draft 2019-09+) + * @see https://tools.ietf.org/doc/html/draft-handrews-json-schema-validation-01#page-22 + */ + definitions?: + | { + [key: string]: JSONSchemaDefinition; + } + | undefined; + + /** + * @see https://json-schema.org/draft/2020-12/json-schema-core#ref + */ + $ref?: string | undefined; + /** * @see https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-7 */ diff --git a/src/lib/transform.ts b/src/lib/transform.ts new file mode 100644 index 000000000..ee0ca84eb --- /dev/null +++ b/src/lib/transform.ts @@ -0,0 +1,165 @@ +import type { JSONSchema, JSONSchemaDefinition } from './jsonschema'; + +export function toStrictJsonSchema(schema: JSONSchema): JSONSchema { + return ensureStrictJsonSchema(schema, [], schema); +} + +function ensureStrictJsonSchema( + jsonSchema: JSONSchemaDefinition, + path: string[], + root: JSONSchema, +): JSONSchema { + /** + * Mutates the given JSON schema to ensure it conforms to the `strict` standard + * that the API expects. + */ + if (typeof jsonSchema === 'boolean') { + throw new TypeError(`Expected object schema but got boolean; path=${path.join('/')}`); + } + + if (!isDict(jsonSchema)) { + throw new TypeError(`Expected ${JSON.stringify(jsonSchema)} to be a dictionary; path=${path.join('/')}`); + } + + // Handle $defs (non-standard but sometimes used) + const defs = (jsonSchema as any).$defs; + if (isDict(defs)) { + for (const [defName, defSchema] of Object.entries(defs)) { + ensureStrictJsonSchema(defSchema, [...path, '$defs', defName], root); + } + } + + // Handle definitions (draft-04 style, deprecated in draft-07 but still used) + const definitions = (jsonSchema as any).definitions; + if (isDict(definitions)) { + for (const [definitionName, definitionSchema] of Object.entries(definitions)) { + ensureStrictJsonSchema(definitionSchema, [...path, 'definitions', definitionName], root); + } + } + + // Add additionalProperties: false to object types + const typ = jsonSchema.type; + if (typ === 'object' && !('additionalProperties' in jsonSchema)) { + jsonSchema.additionalProperties = false; + } + + // Handle object properties + const properties = jsonSchema.properties; + if (isDict(properties)) { + jsonSchema.required = Object.keys(properties); + jsonSchema.properties = Object.fromEntries( + Object.entries(properties).map(([key, propSchema]) => [ + key, + ensureStrictJsonSchema(propSchema, [...path, 'properties', key], root), + ]), + ); + } + + // Handle arrays + const items = jsonSchema.items; + if (isDict(items)) { + // @ts-ignore(2345) + jsonSchema.items = ensureStrictJsonSchema(items, [...path, 'items'], root); + } + + // Handle unions (anyOf) + const anyOf = jsonSchema.anyOf; + if (Array.isArray(anyOf)) { + jsonSchema.anyOf = anyOf.map((variant, i) => + ensureStrictJsonSchema(variant, [...path, 'anyOf', String(i)], root), + ); + } + + // Handle intersections (allOf) + const allOf = jsonSchema.allOf; + if (Array.isArray(allOf)) { + if (allOf.length === 1) { + const resolved = ensureStrictJsonSchema(allOf[0]!, [...path, 'allOf', '0'], root); + Object.assign(jsonSchema, resolved); + delete jsonSchema.allOf; + } else { + jsonSchema.allOf = allOf.map((entry, i) => + ensureStrictJsonSchema(entry, [...path, 'allOf', String(i)], root), + ); + } + } + + // Strip `null` defaults as there's no meaningful distinction + if (jsonSchema.default === null) { + delete jsonSchema.default; + } + + // Handle $ref with additional properties + const ref = (jsonSchema as any).$ref; + if (ref && hasMoreThanNKeys(jsonSchema, 1)) { + if (typeof ref !== 'string') { + throw new TypeError(`Received non-string $ref - ${ref}`); + } + + const resolved = resolveRef(root, ref); + if (typeof resolved === 'boolean') { + throw new ValueError(`Expected \`$ref: ${ref}\` to resolve to an object schema but got boolean`); + } + if (!isDict(resolved)) { + throw new ValueError( + `Expected \`$ref: ${ref}\` to resolve to a dictionary but got ${JSON.stringify(resolved)}`, + ); + } + + // Properties from the json schema take priority over the ones on the `$ref` + Object.assign(jsonSchema, { ...resolved, ...jsonSchema }); + delete (jsonSchema as any).$ref; + + // Since the schema expanded from `$ref` might not have `additionalProperties: false` applied, + // we call `ensureStrictJsonSchema` again to fix the inlined schema and ensure it's valid. + return ensureStrictJsonSchema(jsonSchema, path, root); + } + + return jsonSchema; +} + +function resolveRef(root: JSONSchema, ref: string): JSONSchemaDefinition { + if (!ref.startsWith('#/')) { + throw new ValueError(`Unexpected $ref format ${JSON.stringify(ref)}; Does not start with #/`); + } + + const pathParts = ref.slice(2).split('/'); + let resolved: any = root; + + for (const key of pathParts) { + if (!isDict(resolved)) { + throw new Error( + `encountered non-dictionary entry while resolving ${ref} - ${JSON.stringify(resolved)}`, + ); + } + const value = resolved[key]; + if (value === undefined) { + throw new Error(`Key ${key} not found while resolving ${ref}`); + } + resolved = value; + } + + return resolved; +} + +function isDict(obj: any): obj is Record { + return typeof obj === 'object' && obj !== null && !Array.isArray(obj); +} + +function hasMoreThanNKeys(obj: Record, n: number): boolean { + let i = 0; + for (const _ in obj) { + i++; + if (i > n) { + return true; + } + } + return false; +} + +class ValueError extends Error { + constructor(message: string) { + super(message); + this.name = 'ValueError'; + } +} diff --git a/tests/lib/transform.test.ts b/tests/lib/transform.test.ts new file mode 100644 index 000000000..1cce99b98 --- /dev/null +++ b/tests/lib/transform.test.ts @@ -0,0 +1,535 @@ +import { JSONSchema } from 'openai/lib/jsonschema'; +import { toStrictJsonSchema } from 'openai/lib/transform'; + +describe('Strict JSON Schema Validation', () => { + describe('General Rules', () => { + test('root schema must be an object', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + name: { type: 'string' }, + }, + }; + + const strict = toStrictJsonSchema(schema); + expect(strict.type).toBe('object'); + }); + + test('root schema cannot use anyOf', () => { + const schema: JSONSchema = { + anyOf: [ + { type: 'object', properties: { a: { type: 'string' } } }, + { type: 'object', properties: { b: { type: 'number' } } }, + ], + }; + + // This should be rejected by validation + // In a real implementation, you'd add validation logic + expect(schema.anyOf).toBeDefined(); + expect(schema.type).toBeUndefined(); + }); + + test('all fields must be required', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + name: { type: 'string' }, + age: { type: 'number' }, + email: { type: 'string' }, + }, + }; + + const strict = toStrictJsonSchema(schema); + expect(strict.required).toEqual(['name', 'age', 'email']); + }); + + test('optional fields emulated with union types', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + name: { type: 'string' }, + nickname: { + anyOf: [{ type: 'string' }, { type: 'null' }], + }, + }, + }; + + const strict = toStrictJsonSchema(schema); + expect(strict.required).toContain('nickname'); + + const nickname = strict.properties!['nickname'] as JSONSchema; + expect(nickname.anyOf).toBeDefined(); + }); + + test('additionalProperties: false must be set on all objects', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + nested: { + type: 'object', + properties: { + value: { type: 'string' }, + }, + }, + }, + }; + + const strict = toStrictJsonSchema(schema); + expect(strict.additionalProperties).toBe(false); + + const nested = strict.properties!['nested'] as JSONSchema; + expect(nested.additionalProperties).toBe(false); + }); + + test('key ordering is preserved', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + zebra: { type: 'string' }, + apple: { type: 'string' }, + middle: { type: 'string' }, + }, + }; + + const strict = toStrictJsonSchema(schema); + const keys = Object.keys(strict.properties!); + + expect(keys).toEqual(['zebra', 'apple', 'middle']); + }); + }); + + describe('Supported Property Types & Constraints', () => { + describe('Strings', () => { + test('pattern constraint is preserved', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + code: { + type: 'string', + pattern: '^[A-Z]{3}-\\d{4}$', + }, + }, + }; + + const strict = toStrictJsonSchema(schema); + const code = strict.properties!['code'] as JSONSchema; + expect(code.pattern).toBe('^[A-Z]{3}-\\d{4}$'); + }); + + test('supported format values are preserved', () => { + const formats = [ + 'date-time', + 'time', + 'date', + 'duration', + 'email', + 'hostname', + 'ipv4', + 'ipv6', + 'uuid', + ]; + + formats.forEach((fmt) => { + const schema: JSONSchema = { + type: 'object', + properties: { + field: { + type: 'string', + format: fmt, + }, + }, + }; + + const strict = toStrictJsonSchema(schema); + const field = strict.properties!['field'] as JSONSchema; + expect(field.format).toBe(fmt); + }); + }); + }); + + describe('Numbers', () => { + test('multipleOf constraint is preserved', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + score: { + type: 'number', + multipleOf: 5, + }, + }, + }; + + const strict = toStrictJsonSchema(schema); + const score = strict.properties!['score'] as JSONSchema; + expect(score.multipleOf).toBe(5); + }); + + test('maximum and minimum constraints are preserved', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + age: { + type: 'integer', + minimum: 0, + maximum: 150, + }, + }, + }; + + const strict = toStrictJsonSchema(schema); + const age = strict.properties!['age'] as JSONSchema; + expect(age.minimum).toBe(0); + expect(age.maximum).toBe(150); + }); + + test('exclusive minimum and maximum constraints are preserved', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + rating: { + type: 'number', + exclusiveMinimum: 0, + exclusiveMaximum: 100, + }, + }, + }; + + const strict = toStrictJsonSchema(schema); + const rating = strict.properties!['rating'] as JSONSchema; + expect(rating.exclusiveMinimum).toBe(0); + expect(rating.exclusiveMaximum).toBe(100); + }); + }); + + describe('Arrays', () => { + test('minItems and maxItems constraints are preserved', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + tags: { + type: 'array', + items: { type: 'string' }, + minItems: 1, + maxItems: 10, + }, + }, + }; + + const strict = toStrictJsonSchema(schema); + const tags = strict.properties!['tags'] as JSONSchema; + expect(tags.minItems).toBe(1); + expect(tags.maxItems).toBe(10); + }); + }); + }); + + describe('Object & Schema Limitations', () => { + test('nesting depth up to 10 levels', () => { + // Create a deeply nested schema (10 levels) + const createNested = (depth: number): JSONSchema => { + if (depth === 0) { + return { type: 'string' }; + } + return { + type: 'object', + properties: { + nested: createNested(depth - 1), + }, + }; + }; + + const schema = createNested(10); + const strict = toStrictJsonSchema(schema); + + // Verify it processes without error + expect(strict.type).toBe('object'); + }); + + test('handles many object properties', () => { + const properties: Record = {}; + + // Create 100 properties (testing a reasonable subset of 5000 limit) + for (let i = 0; i < 100; i++) { + properties[`field${i}`] = { type: 'string' }; + } + + const schema: JSONSchema = { + type: 'object', + properties, + }; + + const strict = toStrictJsonSchema(schema); + expect(Object.keys(strict.properties!).length).toBe(100); + expect(strict.required!.length).toBe(100); + }); + + test('handles enum values', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + status: { + type: 'string', + enum: ['pending', 'active', 'completed', 'cancelled'], + }, + }, + }; + + const strict = toStrictJsonSchema(schema); + const status = strict.properties!['status'] as JSONSchema; + expect(status.enum).toEqual(['pending', 'active', 'completed', 'cancelled']); + }); + + test('handles large enum (under 250 values)', () => { + const enumValues = Array.from({ length: 200 }, (_, i) => `value${i}`); + + const schema: JSONSchema = { + type: 'object', + properties: { + code: { + type: 'string', + enum: enumValues, + }, + }, + }; + + const strict = toStrictJsonSchema(schema); + const code = strict.properties!['code'] as JSONSchema; + expect(code.enum?.length).toBe(200); + }); + }); + + describe('Supported Structures', () => { + test('anyOf is allowed within object properties', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + value: { + anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }], + }, + }, + }; + + const strict = toStrictJsonSchema(schema); + const value = strict.properties!['value'] as JSONSchema; + expect(value.anyOf).toBeDefined(); + expect(value.anyOf!.length).toBe(3); + }); + + test('$defs for reusable subschemas', () => { + const schema: JSONSchema & { $defs?: any } = { + type: 'object', + properties: { + user: { $ref: '#/$defs/User' }, + admin: { $ref: '#/$defs/User' }, + }, + $defs: { + User: { + type: 'object', + properties: { + name: { type: 'string' }, + email: { type: 'string', format: 'email' }, + }, + }, + }, + }; + + const strict = toStrictJsonSchema(schema); + expect(strict.$defs).toBeDefined(); + + const userDef = (strict.$defs as any).User as JSONSchema; + expect(userDef.additionalProperties).toBe(false); + expect(userDef.required).toEqual(['name', 'email']); + }); + + test('recursive schemas via root reference', () => { + const schema: JSONSchema & { $defs?: any } = { + type: 'object', + properties: { + value: { type: 'string' }, + children: { + type: 'array', + items: { $ref: '#' }, + }, + }, + }; + + const strict = toStrictJsonSchema(schema); + expect(strict.properties!['children']).toBeDefined(); + + const children = strict.properties!['children'] as JSONSchema; + const items = children.items as JSONSchema & { $ref?: string }; + expect(items.$ref).toBe('#'); + }); + + test('recursive schemas via $defs reference', () => { + const schema: JSONSchema & { $defs?: any } = { + type: 'object', + properties: { + tree: { $ref: '#/$defs/TreeNode' }, + }, + $defs: { + TreeNode: { + type: 'object', + properties: { + value: { type: 'string' }, + left: { $ref: '#/$defs/TreeNode' }, + right: { $ref: '#/$defs/TreeNode' }, + }, + }, + }, + }; + + const strict = toStrictJsonSchema(schema); + const treeDef = (strict.$defs as any).TreeNode as JSONSchema; + expect(treeDef.properties!['left']).toBeDefined(); + expect(treeDef.properties!['right']).toBeDefined(); + }); + }); + + describe('Unsupported Features', () => { + test('allOf is unsupported (but processed by our function)', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + combined: { + allOf: [ + { type: 'object', properties: { a: { type: 'string' } } }, + { type: 'object', properties: { b: { type: 'number' } } }, + ], + }, + }, + }; + + // Our function processes allOf, but in strict mode it should be avoided + const strict = toStrictJsonSchema(schema); + const combined = strict.properties!['combined'] as JSONSchema; + + // Single allOf gets expanded + if (combined.allOf && combined.allOf.length === 1) { + expect(combined.allOf).toBeUndefined(); + } + }); + + test('not keyword is unsupported', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + value: { + not: { type: 'null' }, + }, + } as any, + }; + + // Our function doesn't handle 'not', it would pass through + const strict = toStrictJsonSchema(schema); + expect(strict.properties!['value']).toBeDefined(); + }); + + test('if/then/else is unsupported', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + value: { type: 'string' }, + }, + if: { + properties: { + value: { const: 'special' }, + }, + } as any, + then: { + properties: { + extra: { type: 'string' }, + }, + } as any, + }; + + // Our function doesn't handle if/then/else, it would pass through + const strict = toStrictJsonSchema(schema); + expect(strict.if).toBeDefined(); + }); + + test('patternProperties is unsupported', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + data: { type: 'string' }, + }, + patternProperties: { + '^S_': { type: 'string' }, + '^I_': { type: 'integer' }, + }, + }; + + // Our function doesn't remove patternProperties + const strict = toStrictJsonSchema(schema); + expect(strict.patternProperties).toBeDefined(); + }); + }); + + describe('Fine-tuned Model Restrictions', () => { + test('string constraints that would be restricted', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + username: { + type: 'string', + minLength: 3, + maxLength: 20, + pattern: '^[a-zA-Z0-9_]+$', + }, + }, + }; + + const strict = toStrictJsonSchema(schema); + const username = strict.properties!['username'] as JSONSchema; + + // These are preserved but would be restricted for fine-tuned models + expect(username.minLength).toBe(3); + expect(username.maxLength).toBe(20); + expect(username.pattern).toBeDefined(); + }); + + test('number constraints that would be restricted', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + price: { + type: 'number', + minimum: 0, + maximum: 1000000, + multipleOf: 0.01, + }, + }, + }; + + const strict = toStrictJsonSchema(schema); + const price = strict.properties!['price'] as JSONSchema; + + // These are preserved but would be restricted for fine-tuned models + expect(price.minimum).toBe(0); + expect(price.maximum).toBe(1000000); + expect(price.multipleOf).toBe(0.01); + }); + + test('array constraints that would be restricted', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + items: { + type: 'array', + items: { type: 'string' }, + minItems: 1, + maxItems: 100, + }, + }, + }; + + const strict = toStrictJsonSchema(schema); + const items = strict.properties!['items'] as JSONSchema; + + // These are preserved but would be restricted for fine-tuned models + expect(items.minItems).toBe(1); + expect(items.maxItems).toBe(100); + }); + }); +}); From 7be8d10b70c9ada8480a7d505f03480c2a065661 Mon Sep 17 00:00:00 2001 From: karpetrosyan Date: Wed, 1 Oct 2025 13:01:39 +0400 Subject: [PATCH 2/9] add v4 schema support for zodFunction --- src/helpers/zod.ts | 24 +++++++++++++++++++++--- src/lib/transform.ts | 13 +++---------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/helpers/zod.ts b/src/helpers/zod.ts index 4f0e8bd24..6b65107e4 100644 --- a/src/helpers/zod.ts +++ b/src/helpers/zod.ts @@ -142,14 +142,32 @@ export function zodFunction(options: { arguments: Parameters; name: string; function: (args: zodInfer) => unknown; -}> { - // @ts-expect-error TODO +}>; +export function zodFunction(options: { + name: string; + parameters: Parameters; + function?: ((args: zodInferV4) => unknown | Promise) | undefined; + description?: string | undefined; +}): AutoParseableTool<{ + arguments: Parameters; + name: string; + function: (args: zodInferV4) => unknown; +}>; +export function zodFunction(options: { + name: string; + parameters: Parameters; + function?: ((args: any) => unknown | Promise) | undefined; + description?: string | undefined; +}): unknown { return makeParseableTool( { type: 'function', function: { name: options.name, - parameters: zodToJsonSchema(options.parameters, { name: options.name }), + parameters: + isZodV4(options.parameters) ? + toJSONSchema(options.parameters) + : zodToJsonSchema(options.parameters, { name: options.name }), strict: true, ...(options.description ? { description: options.description } : undefined), }, diff --git a/src/lib/transform.ts b/src/lib/transform.ts index ee0ca84eb..71befa13a 100644 --- a/src/lib/transform.ts +++ b/src/lib/transform.ts @@ -98,10 +98,10 @@ function ensureStrictJsonSchema( const resolved = resolveRef(root, ref); if (typeof resolved === 'boolean') { - throw new ValueError(`Expected \`$ref: ${ref}\` to resolve to an object schema but got boolean`); + throw new Error(`Expected \`$ref: ${ref}\` to resolve to an object schema but got boolean`); } if (!isDict(resolved)) { - throw new ValueError( + throw new Error( `Expected \`$ref: ${ref}\` to resolve to a dictionary but got ${JSON.stringify(resolved)}`, ); } @@ -120,7 +120,7 @@ function ensureStrictJsonSchema( function resolveRef(root: JSONSchema, ref: string): JSONSchemaDefinition { if (!ref.startsWith('#/')) { - throw new ValueError(`Unexpected $ref format ${JSON.stringify(ref)}; Does not start with #/`); + throw new Error(`Unexpected $ref format ${JSON.stringify(ref)}; Does not start with #/`); } const pathParts = ref.slice(2).split('/'); @@ -156,10 +156,3 @@ function hasMoreThanNKeys(obj: Record, n: number): boolean { } return false; } - -class ValueError extends Error { - constructor(message: string) { - super(message); - this.name = 'ValueError'; - } -} From cbf5f81fef33753bbc155624f27644fafa184fb7 Mon Sep 17 00:00:00 2001 From: karpetrosyan Date: Wed, 1 Oct 2025 15:04:02 +0400 Subject: [PATCH 3/9] fixes --- examples/parsing.ts | 2 +- src/helpers/zod.ts | 10 +++++++--- tests/helpers/zod.test.ts | 12 ++++++++---- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/examples/parsing.ts b/examples/parsing.ts index 1290b074c..5eabd7ad6 100644 --- a/examples/parsing.ts +++ b/examples/parsing.ts @@ -1,6 +1,6 @@ import { zodResponseFormat } from 'openai/helpers/zod'; import OpenAI from 'openai/index'; -import { z } from 'zod/v3'; +import { z } from 'zod/v3'; // Also works for 'zod/v4' const Step = z.object({ explanation: z.string(), diff --git a/src/helpers/zod.ts b/src/helpers/zod.ts index 6b65107e4..75fdd23c2 100644 --- a/src/helpers/zod.ts +++ b/src/helpers/zod.ts @@ -25,6 +25,10 @@ function zodToJsonSchema(schema: ZodType, options: { name: string }): Record { + return toJSONSchema(schema, { target: 'draft-7' }) as Record; +} + function isZodV4(zodObject: ZodType | ZodTypeV4): zodObject is ZodTypeV4 { return '_zod' in zodObject; } @@ -90,7 +94,7 @@ export function zodResponseFormat( strict: true, schema: isZodV4(zodObject) ? - (toStrictJsonSchema(toJSONSchema(zodObject) as JSONSchema) as Record) + (toStrictJsonSchema(nativeToJsonSchema(zodObject)) as Record) : zodToJsonSchema(zodObject, { name }), }, }, @@ -121,7 +125,7 @@ export function zodTextFormat( strict: true, schema: isZodV4(zodObject) ? - (toStrictJsonSchema(toJSONSchema(zodObject) as JSONSchema) as Record) + (toStrictJsonSchema(nativeToJsonSchema(zodObject)) as Record) : zodToJsonSchema(zodObject, { name }), }, (content) => zodObject.parse(JSON.parse(content)), @@ -166,7 +170,7 @@ export function zodFunction(options: { name: options.name, parameters: isZodV4(options.parameters) ? - toJSONSchema(options.parameters) + nativeToJsonSchema(options.parameters) : zodToJsonSchema(options.parameters, { name: options.name }), strict: true, ...(options.description ? { description: options.description } : undefined), diff --git a/tests/helpers/zod.test.ts b/tests/helpers/zod.test.ts index 62a8e9162..ba7bcb87b 100644 --- a/tests/helpers/zod.test.ts +++ b/tests/helpers/zod.test.ts @@ -1,7 +1,11 @@ import { zodResponseFormat } from 'openai/helpers/zod'; -import { z } from 'zod/v3'; +import { z as zv3 } from 'zod/v3'; +import { z as zv4 } from 'zod'; -describe('zodResponseFormat', () => { +describe.each([ + { version: 'v3', z: zv3 as any }, + { version: 'v4', z: zv4 as any }, +])('zodResponseFormat (Zod $version)', ({ version, z }) => { it('does the thing', () => { expect( zodResponseFormat( @@ -286,7 +290,7 @@ describe('zodResponseFormat', () => { `); }); - it('throws error on optional fields', () => { + (version === 'v4' ? it.skip : it)('throws error on optional fields', () => { expect(() => zodResponseFormat( z.object({ @@ -301,7 +305,7 @@ describe('zodResponseFormat', () => { ); }); - it('throws error on nested optional fields', () => { + (version === 'v4' ? it.skip : it)('throws error on nested optional fields', () => { expect(() => zodResponseFormat( z.object({ From a1a47181a695f7b83bf09cf6378fb462d7067b9a Mon Sep 17 00:00:00 2001 From: karpetrosyan Date: Thu, 2 Oct 2025 19:09:45 +0400 Subject: [PATCH 4/9] review fixes + add zod@4 support for tool functions --- src/helpers/zod.ts | 94 ++++++++++++++++++++++--------------- src/lib/transform.ts | 28 +++++++++++ tests/helpers/zod.test.ts | 6 +-- tests/lib/transform.test.ts | 39 +++++++++++++-- 4 files changed, 122 insertions(+), 45 deletions(-) diff --git a/src/helpers/zod.ts b/src/helpers/zod.ts index 75fdd23c2..31637378b 100644 --- a/src/helpers/zod.ts +++ b/src/helpers/zod.ts @@ -1,6 +1,6 @@ import { ResponseFormatJSONSchema } from '../resources/index'; -import type { infer as zodInfer, ZodType } from 'zod/v3'; -import { toJSONSchema, type infer as zodInferV4, type ZodType as ZodTypeV4 } from 'zod/v4'; +import { z as z3 } from 'zod/v3'; +import { z as z4 } from 'zod/v4'; import { AutoParseableResponseFormat, AutoParseableTextFormat, @@ -13,9 +13,8 @@ import { zodToJsonSchema as _zodToJsonSchema } from '../_vendor/zod-to-json-sche import { AutoParseableResponseTool, makeParseableResponseTool } from '../lib/ResponsesParser'; import { type ResponseFormatTextJSONSchemaConfig } from '../resources/responses/responses'; import { toStrictJsonSchema } from '../lib/transform'; -import { JSONSchema } from '../lib/jsonschema'; -function zodToJsonSchema(schema: ZodType, options: { name: string }): Record { +function zodV3ToJsonSchema(schema: z3.ZodType, options: { name: string }): Record { return _zodToJsonSchema(schema, { openaiStrictMode: true, name: options.name, @@ -25,11 +24,13 @@ function zodToJsonSchema(schema: ZodType, options: { name: string }): Record { - return toJSONSchema(schema, { target: 'draft-7' }) as Record; +function zodV4ToJsonSchema(schema: z4.ZodType): Record { + return toStrictJsonSchema( + z4.toJSONSchema(schema, { target: 'draft-7' }) as Record, + ) as Record; } -function isZodV4(zodObject: ZodType | ZodTypeV4): zodObject is ZodTypeV4 { +function isZodV4(zodObject: z3.ZodType | z4.ZodType): zodObject is z4.ZodType { return '_zod' in zodObject; } @@ -70,17 +71,17 @@ function isZodV4(zodObject: ZodType | ZodTypeV4): zodObject is ZodTypeV4 { * 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, -): AutoParseableResponseFormat>; -export function zodResponseFormat( +): AutoParseableResponseFormat>; +export function zodResponseFormat( zodObject: ZodInput, name: string, props?: Omit, -): AutoParseableResponseFormat>; -export function zodResponseFormat( +): AutoParseableResponseFormat>; +export function zodResponseFormat( zodObject: ZodInput, name: string, props?: Omit, @@ -92,27 +93,24 @@ export function zodResponseFormat( ...props, name, strict: true, - schema: - isZodV4(zodObject) ? - (toStrictJsonSchema(nativeToJsonSchema(zodObject)) as Record) - : zodToJsonSchema(zodObject, { name }), + schema: isZodV4(zodObject) ? zodV4ToJsonSchema(zodObject) : zodV3ToJsonSchema(zodObject, { name }), }, }, (content) => zodObject.parse(JSON.parse(content)), ); } -export function zodTextFormat( +export function zodTextFormat( zodObject: ZodInput, name: string, props?: Omit, -): AutoParseableTextFormat>; -export function zodTextFormat( +): AutoParseableTextFormat>; +export function zodTextFormat( zodObject: ZodInput, name: string, props?: Omit, -): AutoParseableTextFormat>; -export function zodTextFormat( +): AutoParseableTextFormat>; +export function zodTextFormat( zodObject: ZodInput, name: string, props?: Omit, @@ -123,10 +121,7 @@ export function zodTextFormat( ...props, name, strict: true, - schema: - isZodV4(zodObject) ? - (toStrictJsonSchema(nativeToJsonSchema(zodObject)) as Record) - : zodToJsonSchema(zodObject, { name }), + schema: isZodV4(zodObject) ? zodV4ToJsonSchema(zodObject) : zodV3ToJsonSchema(zodObject, { name }), }, (content) => zodObject.parse(JSON.parse(content)), ); @@ -137,27 +132,27 @@ 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: zodInfer) => unknown | Promise) | undefined; + function?: ((args: z3.infer) => unknown | Promise) | undefined; description?: string | undefined; }): AutoParseableTool<{ arguments: Parameters; name: string; - function: (args: zodInfer) => unknown; + function: (args: z3.infer) => unknown; }>; -export function zodFunction(options: { +export function zodFunction(options: { name: string; parameters: Parameters; - function?: ((args: zodInferV4) => unknown | Promise) | undefined; + function?: ((args: z4.infer) => unknown | Promise) | undefined; description?: string | undefined; }): AutoParseableTool<{ arguments: Parameters; name: string; - function: (args: zodInferV4) => unknown; + function: (args: z4.infer) => unknown; }>; -export function zodFunction(options: { +export function zodFunction(options: { name: string; parameters: Parameters; function?: ((args: any) => unknown | Promise) | undefined; @@ -170,8 +165,8 @@ export function zodFunction(options: { name: options.name, parameters: isZodV4(options.parameters) ? - nativeToJsonSchema(options.parameters) - : zodToJsonSchema(options.parameters, { name: options.name }), + zodV4ToJsonSchema(options.parameters) + : zodV3ToJsonSchema(options.parameters, { name: options.name }), strict: true, ...(options.description ? { description: options.description } : undefined), }, @@ -183,21 +178,44 @@ export function zodFunction(options: { ); } -export function zodResponsesFunction(options: { +export function zodResponsesFunction(options: { name: string; parameters: Parameters; - function?: ((args: zodInfer) => unknown | Promise) | undefined; + function?: ((args: z3.infer) => unknown | Promise) | undefined; description?: string | undefined; }): AutoParseableResponseTool<{ arguments: Parameters; name: string; - function: (args: zodInfer) => unknown; + function: (args: z3.infer) => unknown; +}>; +export function zodResponsesFunction(options: { + name: string; + parameters: Parameters; + function?: ((args: z4.infer) => unknown | Promise) | undefined; + description?: string | undefined; +}): AutoParseableResponseTool<{ + arguments: Parameters; + name: string; + function: (args: z4.infer) => unknown; +}>; +export function zodResponsesFunction(options: { + name: string; + parameters: Parameters; + function?: ((args: unknown) => unknown | Promise) | undefined; + description?: string | undefined; +}): AutoParseableResponseTool<{ + arguments: Parameters; + name: string; + function: (args: unknown) => unknown; }> { return makeParseableResponseTool( { type: 'function', name: options.name, - parameters: zodToJsonSchema(options.parameters, { name: options.name }), + parameters: + isZodV4(options.parameters) ? + zodV4ToJsonSchema(options.parameters) + : zodV3ToJsonSchema(options.parameters, { name: options.name }), strict: true, ...(options.description ? { description: options.description } : undefined), }, diff --git a/src/lib/transform.ts b/src/lib/transform.ts index 71befa13a..1f54074d7 100644 --- a/src/lib/transform.ts +++ b/src/lib/transform.ts @@ -4,6 +4,23 @@ export function toStrictJsonSchema(schema: JSONSchema): JSONSchema { return ensureStrictJsonSchema(schema, [], schema); } +function isNullable(schema: JSONSchemaDefinition): boolean { + if (typeof schema === 'boolean') { + return false; + } + for (const oneOfVariant of schema.oneOf ?? []) { + if (typeof oneOfVariant !== 'boolean' && oneOfVariant.type === 'null') { + return true; + } + } + for (const allOfVariant of schema.anyOf ?? []) { + if (typeof allOfVariant !== 'boolean' && allOfVariant.type === 'null') { + return true; + } + } + return false; +} + function ensureStrictJsonSchema( jsonSchema: JSONSchemaDefinition, path: string[], @@ -43,9 +60,20 @@ function ensureStrictJsonSchema( jsonSchema.additionalProperties = false; } + const required = jsonSchema.required ?? []; + // Handle object properties const properties = jsonSchema.properties; if (isDict(properties)) { + for (const [key, value] of Object.entries(properties)) { + if (!isNullable(value) && !required.includes(key)) { + throw new Error( + `Zod field at \`${['#', 'definitions', 'schema', ...path, 'properties', key].join( + '/', + )}\` uses \`.optional()\` without \`.nullable()\` which is not supported by the API. See: https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#all-fields-must-be-required`, + ); + } + } jsonSchema.required = Object.keys(properties); jsonSchema.properties = Object.fromEntries( Object.entries(properties).map(([key, propSchema]) => [ diff --git a/tests/helpers/zod.test.ts b/tests/helpers/zod.test.ts index ba7bcb87b..c436e9421 100644 --- a/tests/helpers/zod.test.ts +++ b/tests/helpers/zod.test.ts @@ -1,6 +1,6 @@ import { zodResponseFormat } from 'openai/helpers/zod'; import { z as zv3 } from 'zod/v3'; -import { z as zv4 } from 'zod'; +import { z as zv4 } from 'zod/v4'; describe.each([ { version: 'v3', z: zv3 as any }, @@ -290,7 +290,7 @@ describe.each([ `); }); - (version === 'v4' ? it.skip : it)('throws error on optional fields', () => { + it('throws error on optional fields', () => { expect(() => zodResponseFormat( z.object({ @@ -305,7 +305,7 @@ describe.each([ ); }); - (version === 'v4' ? it.skip : it)('throws error on nested optional fields', () => { + it('throws error on nested optional fields', () => { expect(() => zodResponseFormat( z.object({ diff --git a/tests/lib/transform.test.ts b/tests/lib/transform.test.ts index 1cce99b98..756cda8c7 100644 --- a/tests/lib/transform.test.ts +++ b/tests/lib/transform.test.ts @@ -9,6 +9,7 @@ describe('Strict JSON Schema Validation', () => { properties: { name: { type: 'string' }, }, + required: ['name'], }; const strict = toStrictJsonSchema(schema); @@ -18,8 +19,8 @@ describe('Strict JSON Schema Validation', () => { test('root schema cannot use anyOf', () => { const schema: JSONSchema = { anyOf: [ - { type: 'object', properties: { a: { type: 'string' } } }, - { type: 'object', properties: { b: { type: 'number' } } }, + { type: 'object', properties: { a: { type: 'string' } }, required: ['a'] }, + { type: 'object', properties: { b: { type: 'number' } }, required: ['b'] }, ], }; @@ -37,6 +38,7 @@ describe('Strict JSON Schema Validation', () => { age: { type: 'number' }, email: { type: 'string' }, }, + required: ['name', 'age', 'email'], }; const strict = toStrictJsonSchema(schema); @@ -52,6 +54,7 @@ describe('Strict JSON Schema Validation', () => { anyOf: [{ type: 'string' }, { type: 'null' }], }, }, + required: ['name', 'nickname'], }; const strict = toStrictJsonSchema(schema); @@ -70,8 +73,10 @@ describe('Strict JSON Schema Validation', () => { properties: { value: { type: 'string' }, }, + required: ['value'], }, }, + required: ['nested'], }; const strict = toStrictJsonSchema(schema); @@ -89,6 +94,7 @@ describe('Strict JSON Schema Validation', () => { apple: { type: 'string' }, middle: { type: 'string' }, }, + required: ['zebra', 'apple', 'middle'], }; const strict = toStrictJsonSchema(schema); @@ -109,6 +115,7 @@ describe('Strict JSON Schema Validation', () => { pattern: '^[A-Z]{3}-\\d{4}$', }, }, + required: ['code'], }; const strict = toStrictJsonSchema(schema); @@ -138,6 +145,7 @@ describe('Strict JSON Schema Validation', () => { format: fmt, }, }, + required: ['field'], }; const strict = toStrictJsonSchema(schema); @@ -157,6 +165,7 @@ describe('Strict JSON Schema Validation', () => { multipleOf: 5, }, }, + required: ['score'], }; const strict = toStrictJsonSchema(schema); @@ -174,6 +183,7 @@ describe('Strict JSON Schema Validation', () => { maximum: 150, }, }, + required: ['age'], }; const strict = toStrictJsonSchema(schema); @@ -192,6 +202,7 @@ describe('Strict JSON Schema Validation', () => { exclusiveMaximum: 100, }, }, + required: ['rating'], }; const strict = toStrictJsonSchema(schema); @@ -213,6 +224,7 @@ describe('Strict JSON Schema Validation', () => { maxItems: 10, }, }, + required: ['tags'], }; const strict = toStrictJsonSchema(schema); @@ -235,6 +247,7 @@ describe('Strict JSON Schema Validation', () => { properties: { nested: createNested(depth - 1), }, + required: ['nested'], }; }; @@ -256,6 +269,7 @@ describe('Strict JSON Schema Validation', () => { const schema: JSONSchema = { type: 'object', properties, + required: Object.keys(properties), }; const strict = toStrictJsonSchema(schema); @@ -272,6 +286,7 @@ describe('Strict JSON Schema Validation', () => { enum: ['pending', 'active', 'completed', 'cancelled'], }, }, + required: ['status'], }; const strict = toStrictJsonSchema(schema); @@ -290,6 +305,7 @@ describe('Strict JSON Schema Validation', () => { enum: enumValues, }, }, + required: ['code'], }; const strict = toStrictJsonSchema(schema); @@ -307,6 +323,7 @@ describe('Strict JSON Schema Validation', () => { anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }], }, }, + required: ['value'], }; const strict = toStrictJsonSchema(schema); @@ -329,8 +346,10 @@ describe('Strict JSON Schema Validation', () => { name: { type: 'string' }, email: { type: 'string', format: 'email' }, }, + required: ['name', 'email'], }, }, + required: ['user', 'admin'], }; const strict = toStrictJsonSchema(schema); @@ -351,6 +370,7 @@ describe('Strict JSON Schema Validation', () => { items: { $ref: '#' }, }, }, + required: ['value', 'children'], }; const strict = toStrictJsonSchema(schema); @@ -375,8 +395,10 @@ describe('Strict JSON Schema Validation', () => { left: { $ref: '#/$defs/TreeNode' }, right: { $ref: '#/$defs/TreeNode' }, }, + required: ['value', 'left', 'right'], }, }, + required: ['tree'], }; const strict = toStrictJsonSchema(schema); @@ -393,11 +415,12 @@ describe('Strict JSON Schema Validation', () => { properties: { combined: { allOf: [ - { type: 'object', properties: { a: { type: 'string' } } }, - { type: 'object', properties: { b: { type: 'number' } } }, + { type: 'object', properties: { a: { type: 'string' } }, required: ['a'] }, + { type: 'object', properties: { b: { type: 'number' } }, required: ['b'] }, ], }, }, + required: ['combined'], }; // Our function processes allOf, but in strict mode it should be avoided @@ -418,6 +441,7 @@ describe('Strict JSON Schema Validation', () => { not: { type: 'null' }, }, } as any, + required: ['value'], }; // Our function doesn't handle 'not', it would pass through @@ -435,12 +459,15 @@ describe('Strict JSON Schema Validation', () => { properties: { value: { const: 'special' }, }, + required: ['value'], } as any, then: { properties: { extra: { type: 'string' }, }, + required: ['extra'], } as any, + required: ['value'], }; // Our function doesn't handle if/then/else, it would pass through @@ -458,6 +485,7 @@ describe('Strict JSON Schema Validation', () => { '^S_': { type: 'string' }, '^I_': { type: 'integer' }, }, + required: ['data'], }; // Our function doesn't remove patternProperties @@ -478,6 +506,7 @@ describe('Strict JSON Schema Validation', () => { pattern: '^[a-zA-Z0-9_]+$', }, }, + required: ['username'], }; const strict = toStrictJsonSchema(schema); @@ -500,6 +529,7 @@ describe('Strict JSON Schema Validation', () => { multipleOf: 0.01, }, }, + required: ['price'], }; const strict = toStrictJsonSchema(schema); @@ -522,6 +552,7 @@ describe('Strict JSON Schema Validation', () => { maxItems: 100, }, }, + required: ['items'], }; const strict = toStrictJsonSchema(schema); From 19713bfb7b5f9e426aa310f0c73fdb6bf539caa7 Mon Sep 17 00:00:00 2001 From: karpetrosyan Date: Thu, 2 Oct 2025 20:49:04 +0400 Subject: [PATCH 5/9] more tests!! --- examples/parsing-run-tools.ts | 2 +- examples/parsing-stream.ts | 2 +- examples/parsing-tools-stream.ts | 2 +- examples/parsing-tools.ts | 2 +- examples/parsing.ts | 2 +- examples/responses/streaming-tools.ts | 2 +- .../responses/structured-outputs-tools.ts | 2 +- examples/responses/structured-outputs.ts | 2 +- examples/tool-call-helpers-zod.ts | 2 +- examples/ui-generation.ts | 2 +- src/helpers/zod.ts | 18 +- tests/helpers/zod.test.ts | 2 +- tests/lib/ChatCompletionStream.test.ts | 2 +- tests/lib/parser.test.ts | 369 +++++++++++++++++- 14 files changed, 381 insertions(+), 30 deletions(-) diff --git a/examples/parsing-run-tools.ts b/examples/parsing-run-tools.ts index f63758e3b..61b272b43 100644 --- a/examples/parsing-run-tools.ts +++ b/examples/parsing-run-tools.ts @@ -1,5 +1,5 @@ import OpenAI from 'openai'; -import z from 'zod/v3'; +import z from 'zod/v4'; import { zodFunction } from 'openai/helpers/zod'; const Table = z.enum(['orders', 'customers', 'products']); diff --git a/examples/parsing-stream.ts b/examples/parsing-stream.ts index 9a7e9863b..dfa755355 100644 --- a/examples/parsing-stream.ts +++ b/examples/parsing-stream.ts @@ -1,6 +1,6 @@ import { zodResponseFormat } from 'openai/helpers/zod'; import OpenAI from 'openai/index'; -import { z } from 'zod/v3'; +import { z } from 'zod/v4'; const Step = z.object({ explanation: z.string(), diff --git a/examples/parsing-tools-stream.ts b/examples/parsing-tools-stream.ts index 54e97df7a..dc0978468 100644 --- a/examples/parsing-tools-stream.ts +++ b/examples/parsing-tools-stream.ts @@ -1,6 +1,6 @@ import { zodFunction } from 'openai/helpers/zod'; import OpenAI from 'openai/index'; -import { z } from 'zod/v3'; +import { z } from 'zod/v4'; const GetWeatherArgs = z.object({ city: z.string(), diff --git a/examples/parsing-tools.ts b/examples/parsing-tools.ts index 4fd466395..e80bfbc97 100644 --- a/examples/parsing-tools.ts +++ b/examples/parsing-tools.ts @@ -1,6 +1,6 @@ import { zodFunction } from 'openai/helpers/zod'; import OpenAI from 'openai/index'; -import { z } from 'zod/v3'; +import { z } from 'zod/v4'; const Table = z.enum(['orders', 'customers', 'products']); diff --git a/examples/parsing.ts b/examples/parsing.ts index 5eabd7ad6..dead0895e 100644 --- a/examples/parsing.ts +++ b/examples/parsing.ts @@ -1,6 +1,6 @@ import { zodResponseFormat } from 'openai/helpers/zod'; import OpenAI from 'openai/index'; -import { z } from 'zod/v3'; // Also works for 'zod/v4' +import { z } from 'zod/v4'; // Also works for 'zod/v3' const Step = z.object({ explanation: z.string(), diff --git a/examples/responses/streaming-tools.ts b/examples/responses/streaming-tools.ts index b62a4edc0..2a80ebd2f 100755 --- a/examples/responses/streaming-tools.ts +++ b/examples/responses/streaming-tools.ts @@ -2,7 +2,7 @@ import { OpenAI } from 'openai'; import { zodResponsesFunction } from 'openai/helpers/zod'; -import { z } from 'zod/v3'; +import { z } from 'zod/v4'; const Table = z.enum(['orders', 'customers', 'products']); const Column = z.enum([ diff --git a/examples/responses/structured-outputs-tools.ts b/examples/responses/structured-outputs-tools.ts index 9605fd6eb..bc98528d1 100755 --- a/examples/responses/structured-outputs-tools.ts +++ b/examples/responses/structured-outputs-tools.ts @@ -2,7 +2,7 @@ import { OpenAI } from 'openai'; import { zodResponsesFunction } from 'openai/helpers/zod'; -import { z } from 'zod/v3'; +import { z } from 'zod/v4'; const Table = z.enum(['orders', 'customers', 'products']); const Column = z.enum([ diff --git a/examples/responses/structured-outputs.ts b/examples/responses/structured-outputs.ts index e1de6f219..ce00aa8ee 100755 --- a/examples/responses/structured-outputs.ts +++ b/examples/responses/structured-outputs.ts @@ -2,7 +2,7 @@ import { OpenAI } from 'openai'; import { zodTextFormat } from 'openai/helpers/zod'; -import { z } from 'zod/v3'; +import { z } from 'zod/v4'; const Step = z.object({ explanation: z.string(), diff --git a/examples/tool-call-helpers-zod.ts b/examples/tool-call-helpers-zod.ts index 162b54946..f3bbd79e8 100755 --- a/examples/tool-call-helpers-zod.ts +++ b/examples/tool-call-helpers-zod.ts @@ -2,7 +2,7 @@ import OpenAI from 'openai'; import { zodFunction } from 'openai/helpers/zod'; -import { z } from 'zod/v3'; +import { z } from 'zod/v4'; // gets API Key from environment variable OPENAI_API_KEY const openai = new OpenAI(); diff --git a/examples/ui-generation.ts b/examples/ui-generation.ts index 4e61e1f17..d2169a3c1 100644 --- a/examples/ui-generation.ts +++ b/examples/ui-generation.ts @@ -1,5 +1,5 @@ import OpenAI from 'openai'; -import { z } from 'zod/v3'; +import { z } from 'zod/v4'; import { zodResponseFormat } from 'openai/helpers/zod'; const openai = new OpenAI(); diff --git a/src/helpers/zod.ts b/src/helpers/zod.ts index 31637378b..5a8709bc2 100644 --- a/src/helpers/zod.ts +++ b/src/helpers/zod.ts @@ -24,9 +24,11 @@ function zodV3ToJsonSchema(schema: z3.ZodType, options: { name: string }): Recor }); } -function zodV4ToJsonSchema(schema: z4.ZodType): Record { +function zodV4ToJsonSchema(schema: z4.ZodType, options: { name: string }): Record { return toStrictJsonSchema( - z4.toJSONSchema(schema, { target: 'draft-7' }) as Record, + z4.toJSONSchema(schema, { + target: 'draft-7', + }) as Record, ) as Record; } @@ -93,7 +95,10 @@ export function zodResponseFormat( ...props, name, strict: true, - schema: isZodV4(zodObject) ? zodV4ToJsonSchema(zodObject) : zodV3ToJsonSchema(zodObject, { name }), + schema: + isZodV4(zodObject) ? + zodV4ToJsonSchema(zodObject, { name }) + : zodV3ToJsonSchema(zodObject, { name }), }, }, (content) => zodObject.parse(JSON.parse(content)), @@ -121,7 +126,8 @@ export function zodTextFormat( ...props, name, strict: true, - schema: isZodV4(zodObject) ? zodV4ToJsonSchema(zodObject) : zodV3ToJsonSchema(zodObject, { name }), + schema: + isZodV4(zodObject) ? zodV4ToJsonSchema(zodObject, { name }) : zodV3ToJsonSchema(zodObject, { name }), }, (content) => zodObject.parse(JSON.parse(content)), ); @@ -165,7 +171,7 @@ export function zodFunction(options: name: options.name, parameters: isZodV4(options.parameters) ? - zodV4ToJsonSchema(options.parameters) + zodV4ToJsonSchema(options.parameters, { name: options.name }) : zodV3ToJsonSchema(options.parameters, { name: options.name }), strict: true, ...(options.description ? { description: options.description } : undefined), @@ -214,7 +220,7 @@ export function zodResponsesFunction name: options.name, parameters: isZodV4(options.parameters) ? - zodV4ToJsonSchema(options.parameters) + zodV4ToJsonSchema(options.parameters, { name: options.name }) : zodV3ToJsonSchema(options.parameters, { name: options.name }), strict: true, ...(options.description ? { description: options.description } : undefined), diff --git a/tests/helpers/zod.test.ts b/tests/helpers/zod.test.ts index c436e9421..3876dafa5 100644 --- a/tests/helpers/zod.test.ts +++ b/tests/helpers/zod.test.ts @@ -1,5 +1,5 @@ import { zodResponseFormat } from 'openai/helpers/zod'; -import { z as zv3 } from 'zod/v3'; +import { z as zv3 } from 'zod/v4'; import { z as zv4 } from 'zod/v4'; describe.each([ diff --git a/tests/lib/ChatCompletionStream.test.ts b/tests/lib/ChatCompletionStream.test.ts index 790736bc0..444b144c3 100644 --- a/tests/lib/ChatCompletionStream.test.ts +++ b/tests/lib/ChatCompletionStream.test.ts @@ -1,6 +1,6 @@ import { zodResponseFormat } from 'openai/helpers/zod'; import { ChatCompletionTokenLogprob } from 'openai/resources'; -import { z } from 'zod/v3'; +import { z } from 'zod/v4'; import { makeStreamSnapshotRequest } from '../utils/mock-snapshots'; jest.setTimeout(1000 * 30); diff --git a/tests/lib/parser.test.ts b/tests/lib/parser.test.ts index 70d3ec8c6..fc6194dcd 100644 --- a/tests/lib/parser.test.ts +++ b/tests/lib/parser.test.ts @@ -1,10 +1,14 @@ -import { z } from 'zod/v3'; +import { z as z4 } from 'zod/v4'; +import { z as z3 } from 'zod/v3'; import { zodResponseFormat } from 'openai/helpers/zod'; import { makeSnapshotRequest } from '../utils/mock-snapshots'; jest.setTimeout(1000 * 30); -describe('.parse()', () => { +describe.each([ + { version: 'v4', z: z4 as any }, + { version: 'v3', z: z3 as any }, +])('.parse()', ({ z, version }) => { describe('zod', () => { it('deserialises response_format', async () => { const completion = await makeSnapshotRequest((openai) => @@ -156,7 +160,8 @@ describe('.parse()', () => { } `); - expect(zodResponseFormat(UI, 'ui').json_schema).toMatchInlineSnapshot(` + if (version === 'v3') { + expect(zodResponseFormat(UI, 'ui').json_schema).toMatchInlineSnapshot(` { "name": "ui", "schema": { @@ -267,6 +272,66 @@ describe('.parse()', () => { "strict": true, } `); + } else { + expect(zodResponseFormat(UI, 'ui').json_schema).toMatchInlineSnapshot(` +{ + "name": "ui", + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "attributes": { + "items": { + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + }, + "value": { + "type": "string", + }, + }, + "required": [ + "name", + "value", + ], + "type": "object", + }, + "type": "array", + }, + "children": { + "items": { + "$ref": "#", + }, + "type": "array", + }, + "label": { + "type": "string", + }, + "type": { + "enum": [ + "div", + "button", + "header", + "section", + "field", + "form", + ], + "type": "string", + }, + }, + "required": [ + "type", + "label", + "children", + "attributes", + ], + "type": "object", + }, + "strict": true, +} +`); + } }); test('merged schemas', async () => { @@ -294,8 +359,9 @@ describe('.parse()', () => { ), }); - expect(zodResponseFormat(contactPersonSchema, 'contactPerson').json_schema.schema) - .toMatchInlineSnapshot(` + if (version === 'v3') { + expect(zodResponseFormat(contactPersonSchema, 'contactPerson').json_schema.schema) + .toMatchInlineSnapshot(` { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, @@ -445,6 +511,100 @@ describe('.parse()', () => { "type": "object", } `); + } else { + expect(zodResponseFormat(contactPersonSchema, 'contactPerson').json_schema.schema) + .toMatchInlineSnapshot(` +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "person1": { + "additionalProperties": false, + "properties": { + "description": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + "description": "Open text for any other relevant information about what the contact does.", + }, + "name": { + "type": "string", + }, + "phone_number": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + }, + "roles": { + "description": "Any roles for which the contact is important, use other for custom roles", + "items": { + "enum": [ + "parent", + "child", + "sibling", + "spouse", + "friend", + "other", + ], + "type": "string", + }, + "type": "array", + }, + }, + "required": [ + "name", + "phone_number", + "roles", + "description", + ], + "type": "object", + }, + "person2": { + "additionalProperties": false, + "properties": { + "differentField": { + "type": "string", + }, + "name": { + "type": "string", + }, + "phone_number": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + }, + }, + "required": [ + "name", + "phone_number", + "differentField", + ], + "type": "object", + }, + }, + "required": [ + "person1", + "person2", + ], + "type": "object", +} +`); + } const completion = await makeSnapshotRequest( (openai) => @@ -517,7 +677,8 @@ describe('.parse()', () => { fields: z.array(z.union([fieldA, fieldB])), }); - expect(zodResponseFormat(model, 'query').json_schema.schema).toMatchInlineSnapshot(` + if (version === 'v3') { + expect(zodResponseFormat(model, 'query').json_schema.schema).toMatchInlineSnapshot(` { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, @@ -695,6 +856,101 @@ describe('.parse()', () => { "type": "object", } `); + } else { + expect(zodResponseFormat(model, 'query').json_schema.schema).toMatchInlineSnapshot(` +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "fields": { + "items": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "metadata": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "foo": { + "type": "string", + }, + }, + "required": [ + "foo", + ], + "type": "object", + }, + { + "type": "null", + }, + ], + }, + "name": { + "type": "string", + }, + "type": { + "const": "string", + "type": "string", + }, + }, + "required": [ + "type", + "name", + "metadata", + ], + "type": "object", + }, + { + "additionalProperties": false, + "properties": { + "metadata": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "foo": { + "type": "string", + }, + }, + "required": [ + "foo", + ], + "type": "object", + }, + { + "type": "null", + }, + ], + }, + "type": { + "const": "number", + "type": "string", + }, + }, + "required": [ + "type", + "metadata", + ], + "type": "object", + }, + ], + }, + "type": "array", + }, + "name": { + "type": "string", + }, + }, + "required": [ + "name", + "fields", + ], + "type": "object", +} +`); + } const completion = await makeSnapshotRequest( (openai) => @@ -792,9 +1048,11 @@ describe('.parse()', () => { const baseLinkedListNodeSchema = z.object({ value: z.number(), }); + //@ts-ignore(2503) type LinkedListNode = z.infer & { next: LinkedListNode | null; }; + //@ts-ignore(2503) const linkedListNodeSchema: z.ZodType = baseLinkedListNodeSchema.extend({ next: z.lazy(() => z.union([linkedListNodeSchema, z.null()])), }); @@ -804,7 +1062,8 @@ describe('.parse()', () => { linked_list: linkedListNodeSchema, }); - expect(zodResponseFormat(mainSchema, 'query').json_schema.schema).toMatchInlineSnapshot(` + if (version === 'v3') { + expect(zodResponseFormat(mainSchema, 'query').json_schema.schema).toMatchInlineSnapshot(` { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, @@ -902,6 +1161,48 @@ describe('.parse()', () => { "type": "object", } `); + } else { + expect(zodResponseFormat(mainSchema, 'query').json_schema.schema).toMatchInlineSnapshot(` +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "definitions": { + "__schema0": { + "additionalProperties": false, + "properties": { + "next": { + "anyOf": [ + { + "$ref": "#/definitions/__schema0", + }, + { + "type": "null", + }, + ], + }, + "value": { + "type": "number", + }, + }, + "required": [ + "value", + "next", + ], + "type": "object", + }, + }, + "properties": { + "linked_list": { + "$ref": "#/definitions/__schema0", + }, + }, + "required": [ + "linked_list", + ], + "type": "object", +} +`); + } const completion = await makeSnapshotRequest( (openai) => @@ -948,16 +1249,21 @@ describe('.parse()', () => { }); test('ref schemas with `.transform()`', async () => { - const Inner = z.object({ - baz: z.boolean().transform((v) => v ?? true), + let Inner = z.object({ + baz: + version === 'v3' ? + z.boolean().transform((v: any) => v ?? true) + : z + .boolean() + .transform((v: any) => v ?? true) + .pipe(z.boolean()), }); - const Outer = z.object({ first: Inner, second: Inner, }); - - expect(zodResponseFormat(Outer, 'data').json_schema.schema).toMatchInlineSnapshot(` + if (version === 'v3') { + expect(zodResponseFormat(Outer, 'data').json_schema.schema).toMatchInlineSnapshot(` { "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, @@ -1027,6 +1333,45 @@ describe('.parse()', () => { "type": "object", } `); + } else { + expect(zodResponseFormat(Outer, 'data').json_schema.schema).toMatchInlineSnapshot(` +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "first": { + "additionalProperties": false, + "properties": { + "baz": { + "type": "boolean", + }, + }, + "required": [ + "baz", + ], + "type": "object", + }, + "second": { + "additionalProperties": false, + "properties": { + "baz": { + "type": "boolean", + }, + }, + "required": [ + "baz", + ], + "type": "object", + }, + }, + "required": [ + "first", + "second", + ], + "type": "object", +} +`); + } const completion = await makeSnapshotRequest( (openai) => From 747c0df8379ceaf4c1afb7ab88fb7e19b559a82b Mon Sep 17 00:00:00 2001 From: karpetrosyan Date: Thu, 2 Oct 2025 21:06:03 +0400 Subject: [PATCH 6/9] improve tests --- src/lib/transform.ts | 2 +- tests/helpers/zod.test.ts | 74 +++++++++++++++++++++++++++------------ 2 files changed, 52 insertions(+), 24 deletions(-) diff --git a/src/lib/transform.ts b/src/lib/transform.ts index 1f54074d7..dc493902f 100644 --- a/src/lib/transform.ts +++ b/src/lib/transform.ts @@ -68,7 +68,7 @@ function ensureStrictJsonSchema( for (const [key, value] of Object.entries(properties)) { if (!isNullable(value) && !required.includes(key)) { throw new Error( - `Zod field at \`${['#', 'definitions', 'schema', ...path, 'properties', key].join( + `Zod field at \`${[...path, 'properties', key].join( '/', )}\` uses \`.optional()\` without \`.nullable()\` which is not supported by the API. See: https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#all-fields-must-be-required`, ); diff --git a/tests/helpers/zod.test.ts b/tests/helpers/zod.test.ts index 3876dafa5..afce4e8c4 100644 --- a/tests/helpers/zod.test.ts +++ b/tests/helpers/zod.test.ts @@ -1,5 +1,5 @@ import { zodResponseFormat } from 'openai/helpers/zod'; -import { z as zv3 } from 'zod/v4'; +import { z as zv3 } from 'zod/v3'; import { z as zv4 } from 'zod/v4'; describe.each([ @@ -291,31 +291,59 @@ describe.each([ }); it('throws error on optional fields', () => { - expect(() => - zodResponseFormat( - z.object({ - required: z.string(), - optional: z.string().optional(), - optional_and_nullable: z.string().optional().nullable(), - }), - 'schema', - ), - ).toThrowErrorMatchingInlineSnapshot( - `"Zod field at \`#/definitions/schema/properties/optional\` uses \`.optional()\` without \`.nullable()\` which is not supported by the API. See: https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#all-fields-must-be-required"`, - ); + if (version === 'v3') { + expect(() => + zodResponseFormat( + z.object({ + required: z.string(), + optional: z.string().optional(), + optional_and_nullable: z.string().optional().nullable(), + }), + 'schema', + ), + ).toThrowErrorMatchingInlineSnapshot( + `"Zod field at \`#/definitions/schema/properties/optional\` uses \`.optional()\` without \`.nullable()\` which is not supported by the API. See: https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#all-fields-must-be-required"`, + ); + } else { + expect(() => + zodResponseFormat( + z.object({ + required: z.string(), + optional: z.string().optional(), + optional_and_nullable: z.string().optional().nullable(), + }), + 'schema', + ), + ).toThrowErrorMatchingInlineSnapshot( + `"Zod field at \`properties/optional\` uses \`.optional()\` without \`.nullable()\` which is not supported by the API. See: https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#all-fields-must-be-required"`, + ); + } }); it('throws error on nested optional fields', () => { - expect(() => - zodResponseFormat( - z.object({ - foo: z.object({ bar: z.array(z.object({ can_be_missing: z.boolean().optional() })) }), - }), - 'schema', - ), - ).toThrowErrorMatchingInlineSnapshot( - `"Zod field at \`#/definitions/schema/properties/foo/properties/bar/items/properties/can_be_missing\` uses \`.optional()\` without \`.nullable()\` which is not supported by the API. See: https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#all-fields-must-be-required"`, - ); + if (version === 'v3') { + expect(() => + zodResponseFormat( + z.object({ + foo: z.object({ bar: z.array(z.object({ can_be_missing: z.boolean().optional() })) }), + }), + 'schema', + ), + ).toThrowErrorMatchingInlineSnapshot( + `"Zod field at \`#/definitions/schema/properties/foo/properties/bar/items/properties/can_be_missing\` uses \`.optional()\` without \`.nullable()\` which is not supported by the API. See: https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#all-fields-must-be-required"`, + ); + } else { + expect(() => + zodResponseFormat( + z.object({ + foo: z.object({ bar: z.array(z.object({ can_be_missing: z.boolean().optional() })) }), + }), + 'schema', + ), + ).toThrowErrorMatchingInlineSnapshot( + `"Zod field at \`properties/foo/properties/bar/items/properties/can_be_missing\` uses \`.optional()\` without \`.nullable()\` which is not supported by the API. See: https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#all-fields-must-be-required"`, + ); + } }); it('does not warn on union nullable fields', () => { From 284cfe728140d2cfcb29e21706764c4e076cc3fb Mon Sep 17 00:00:00 2001 From: karpetrosyan Date: Thu, 2 Oct 2025 21:17:47 +0400 Subject: [PATCH 7/9] fix cast --- src/helpers/zod.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/helpers/zod.ts b/src/helpers/zod.ts index 5a8709bc2..16acd6089 100644 --- a/src/helpers/zod.ts +++ b/src/helpers/zod.ts @@ -13,6 +13,7 @@ import { zodToJsonSchema as _zodToJsonSchema } from '../_vendor/zod-to-json-sche import { AutoParseableResponseTool, makeParseableResponseTool } from '../lib/ResponsesParser'; import { type ResponseFormatTextJSONSchemaConfig } from '../resources/responses/responses'; import { toStrictJsonSchema } from '../lib/transform'; +import { JSONSchema } from '../lib/jsonschema'; function zodV3ToJsonSchema(schema: z3.ZodType, options: { name: string }): Record { return _zodToJsonSchema(schema, { @@ -28,7 +29,7 @@ function zodV4ToJsonSchema(schema: z4.ZodType, options: { name: string }): Recor return toStrictJsonSchema( z4.toJSONSchema(schema, { target: 'draft-7', - }) as Record, + }) as JSONSchema, ) as Record; } From 7a98adb717824ffdf04df0391f6d9188257043dd Mon Sep 17 00:00:00 2001 From: karpetrosyan Date: Fri, 3 Oct 2025 12:15:03 +0400 Subject: [PATCH 8/9] remove name parameter from zodV4ToJsonSchema --- src/helpers/zod.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/helpers/zod.ts b/src/helpers/zod.ts index 16acd6089..5f5807776 100644 --- a/src/helpers/zod.ts +++ b/src/helpers/zod.ts @@ -25,7 +25,7 @@ function zodV3ToJsonSchema(schema: z3.ZodType, options: { name: string }): Recor }); } -function zodV4ToJsonSchema(schema: z4.ZodType, options: { name: string }): Record { +function zodV4ToJsonSchema(schema: z4.ZodType): Record { return toStrictJsonSchema( z4.toJSONSchema(schema, { target: 'draft-7', @@ -96,10 +96,7 @@ export function zodResponseFormat( ...props, name, strict: true, - schema: - isZodV4(zodObject) ? - zodV4ToJsonSchema(zodObject, { name }) - : zodV3ToJsonSchema(zodObject, { name }), + schema: isZodV4(zodObject) ? zodV4ToJsonSchema(zodObject) : zodV3ToJsonSchema(zodObject, { name }), }, }, (content) => zodObject.parse(JSON.parse(content)), @@ -127,8 +124,7 @@ export function zodTextFormat( ...props, name, strict: true, - schema: - isZodV4(zodObject) ? zodV4ToJsonSchema(zodObject, { name }) : zodV3ToJsonSchema(zodObject, { name }), + schema: isZodV4(zodObject) ? zodV4ToJsonSchema(zodObject) : zodV3ToJsonSchema(zodObject, { name }), }, (content) => zodObject.parse(JSON.parse(content)), ); @@ -172,7 +168,7 @@ export function zodFunction(options: name: options.name, parameters: isZodV4(options.parameters) ? - zodV4ToJsonSchema(options.parameters, { name: options.name }) + zodV4ToJsonSchema(options.parameters) : zodV3ToJsonSchema(options.parameters, { name: options.name }), strict: true, ...(options.description ? { description: options.description } : undefined), @@ -221,7 +217,7 @@ export function zodResponsesFunction name: options.name, parameters: isZodV4(options.parameters) ? - zodV4ToJsonSchema(options.parameters, { name: options.name }) + zodV4ToJsonSchema(options.parameters) : zodV3ToJsonSchema(options.parameters, { name: options.name }), strict: true, ...(options.description ? { description: options.description } : undefined), From a324c804504f557aa0726de708f3fe593efab35d Mon Sep 17 00:00:00 2001 From: karpetrosyan Date: Fri, 3 Oct 2025 12:25:59 +0400 Subject: [PATCH 9/9] mention zod3/zod4 support in all eaxmples --- examples/parsing-run-tools.ts | 2 +- examples/parsing-stream.ts | 2 +- examples/parsing-tools-stream.ts | 2 +- examples/parsing-tools.ts | 2 +- examples/responses/streaming-tools.ts | 2 +- examples/responses/structured-outputs-tools.ts | 2 +- examples/responses/structured-outputs.ts | 2 +- examples/tool-call-helpers-zod.ts | 2 +- examples/ui-generation.ts | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/parsing-run-tools.ts b/examples/parsing-run-tools.ts index 61b272b43..a8919f1e7 100644 --- a/examples/parsing-run-tools.ts +++ b/examples/parsing-run-tools.ts @@ -1,5 +1,5 @@ import OpenAI from 'openai'; -import z from 'zod/v4'; +import z from 'zod/v4'; // Also works for 'zod/v3' import { zodFunction } from 'openai/helpers/zod'; const Table = z.enum(['orders', 'customers', 'products']); diff --git a/examples/parsing-stream.ts b/examples/parsing-stream.ts index dfa755355..4bac67006 100644 --- a/examples/parsing-stream.ts +++ b/examples/parsing-stream.ts @@ -1,6 +1,6 @@ import { zodResponseFormat } from 'openai/helpers/zod'; import OpenAI from 'openai/index'; -import { z } from 'zod/v4'; +import { z } from 'zod/v4'; // Also works for 'zod/v3' const Step = z.object({ explanation: z.string(), diff --git a/examples/parsing-tools-stream.ts b/examples/parsing-tools-stream.ts index dc0978468..1d6cd115c 100644 --- a/examples/parsing-tools-stream.ts +++ b/examples/parsing-tools-stream.ts @@ -1,6 +1,6 @@ import { zodFunction } from 'openai/helpers/zod'; import OpenAI from 'openai/index'; -import { z } from 'zod/v4'; +import { z } from 'zod/v4'; // Also works for 'zod/v3' const GetWeatherArgs = z.object({ city: z.string(), diff --git a/examples/parsing-tools.ts b/examples/parsing-tools.ts index e80bfbc97..164283615 100644 --- a/examples/parsing-tools.ts +++ b/examples/parsing-tools.ts @@ -1,6 +1,6 @@ import { zodFunction } from 'openai/helpers/zod'; import OpenAI from 'openai/index'; -import { z } from 'zod/v4'; +import { z } from 'zod/v4'; // Also works for 'zod/v3' const Table = z.enum(['orders', 'customers', 'products']); diff --git a/examples/responses/streaming-tools.ts b/examples/responses/streaming-tools.ts index 2a80ebd2f..838e87bc2 100755 --- a/examples/responses/streaming-tools.ts +++ b/examples/responses/streaming-tools.ts @@ -2,7 +2,7 @@ import { OpenAI } from 'openai'; import { zodResponsesFunction } from 'openai/helpers/zod'; -import { z } from 'zod/v4'; +import { z } from 'zod/v4'; // Also works for 'zod/v3' const Table = z.enum(['orders', 'customers', 'products']); const Column = z.enum([ diff --git a/examples/responses/structured-outputs-tools.ts b/examples/responses/structured-outputs-tools.ts index bc98528d1..4687df49c 100755 --- a/examples/responses/structured-outputs-tools.ts +++ b/examples/responses/structured-outputs-tools.ts @@ -2,7 +2,7 @@ import { OpenAI } from 'openai'; import { zodResponsesFunction } from 'openai/helpers/zod'; -import { z } from 'zod/v4'; +import { z } from 'zod/v4'; // Also works for 'zod/v3' const Table = z.enum(['orders', 'customers', 'products']); const Column = z.enum([ diff --git a/examples/responses/structured-outputs.ts b/examples/responses/structured-outputs.ts index ce00aa8ee..2defb58a9 100755 --- a/examples/responses/structured-outputs.ts +++ b/examples/responses/structured-outputs.ts @@ -2,7 +2,7 @@ import { OpenAI } from 'openai'; import { zodTextFormat } from 'openai/helpers/zod'; -import { z } from 'zod/v4'; +import { z } from 'zod/v4'; // Also works for 'zod/v3' const Step = z.object({ explanation: z.string(), diff --git a/examples/tool-call-helpers-zod.ts b/examples/tool-call-helpers-zod.ts index f3bbd79e8..297171dde 100755 --- a/examples/tool-call-helpers-zod.ts +++ b/examples/tool-call-helpers-zod.ts @@ -2,7 +2,7 @@ import OpenAI from 'openai'; import { zodFunction } from 'openai/helpers/zod'; -import { z } from 'zod/v4'; +import { z } from 'zod/v4'; // Also works for 'zod/v3' // gets API Key from environment variable OPENAI_API_KEY const openai = new OpenAI(); diff --git a/examples/ui-generation.ts b/examples/ui-generation.ts index d2169a3c1..e1c7cd2de 100644 --- a/examples/ui-generation.ts +++ b/examples/ui-generation.ts @@ -1,5 +1,5 @@ import OpenAI from 'openai'; -import { z } from 'zod/v4'; +import { z } from 'zod/v4'; // Also works for 'zod/v3' import { zodResponseFormat } from 'openai/helpers/zod'; const openai = new OpenAI();