Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8f56474
feat: add support for zod@4 schemas
karpetrosyan Oct 1, 2025
7be8d10
add v4 schema support for zodFunction
karpetrosyan Oct 1, 2025
cbf5f81
fixes
karpetrosyan Oct 1, 2025
6b4ae77
Merge branch 'next' into support-zod-v4-schemas
karpetrosyan Oct 2, 2025
a1a4718
review fixes + add zod@4 support for tool functions
karpetrosyan Oct 2, 2025
19713bf
more tests!!
karpetrosyan Oct 2, 2025
747c0df
improve tests
karpetrosyan Oct 2, 2025
284cfe7
fix cast
karpetrosyan Oct 2, 2025
7a98adb
remove name parameter from zodV4ToJsonSchema
karpetrosyan Oct 3, 2025
98e5b8c
Merge branch 'next' into support-zod-v4-schemas
karpetrosyan Oct 3, 2025
a324c80
mention zod3/zod4 support in all eaxmples
karpetrosyan Oct 3, 2025
a532410
chore(internal): use npm pack for build uploads
stainless-app[bot] Oct 6, 2025
f3ab503
Merge branch 'next' into support-zod-v4-schemas
karpetrosyan Oct 7, 2025
946a7b0
Merge branch 'next' into support-zod-v4-schemas
karpetrosyan Oct 16, 2025
d4aaef9
fix(api): internal openapi updates
stainless-app[bot] Oct 17, 2025
2ddb9c9
Merge branch 'next' into support-zod-v4-schemas
karpetrosyan Oct 20, 2025
704ba4c
Merge branch 'next' into support-zod-v4-schemas
karpetrosyan Oct 20, 2025
1192cb2
review fixes
karpetrosyan Oct 23, 2025
646c820
Merge branch 'next' into support-zod-v4-schemas
karpetrosyan Oct 23, 2025
dfed1f0
remove AI traces
karpetrosyan Oct 24, 2025
195a6ed
Update tests/lib/transform.test.ts
karpetrosyan Oct 24, 2025
79a8c53
improve types
RobertCraigie Oct 24, 2025
2fbb1ce
fix doc comment
RobertCraigie Oct 24, 2025
ddeb05e
rename isDict
RobertCraigie Oct 24, 2025
95ce1a9
improve error
RobertCraigie Oct 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/parsing-run-tools.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import OpenAI from 'openai';
import z from 'zod/v3';
import z from 'zod/v4'; // Also works for 'zod/v3'
import { zodFunction } from 'openai/helpers/zod';

const Table = z.enum(['orders', 'customers', 'products']);
Expand Down
2 changes: 1 addition & 1 deletion examples/parsing-stream.ts
Original file line number Diff line number Diff line change
@@ -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'; // Also works for 'zod/v3'

const Step = z.object({
explanation: z.string(),
Expand Down
2 changes: 1 addition & 1 deletion examples/parsing-tools-stream.ts
Original file line number Diff line number Diff line change
@@ -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'; // Also works for 'zod/v3'

const GetWeatherArgs = z.object({
city: z.string(),
Expand Down
2 changes: 1 addition & 1 deletion examples/parsing-tools.ts
Original file line number Diff line number Diff line change
@@ -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'; // Also works for 'zod/v3'

const Table = z.enum(['orders', 'customers', 'products']);

Expand Down
2 changes: 1 addition & 1 deletion examples/parsing.ts
Original file line number Diff line number Diff line change
@@ -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'; // Also works for 'zod/v3'

const Step = z.object({
explanation: z.string(),
Expand Down
2 changes: 1 addition & 1 deletion examples/responses/streaming-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { OpenAI } from 'openai';
import { zodResponsesFunction } from 'openai/helpers/zod';
import { z } from 'zod/v3';
import { z } from 'zod/v4'; // Also works for 'zod/v3'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@karpetrosyan any chance you could run all these examples with v3 on this PR just to make sure things are back-compatible?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah thats a good callout, I think I asked to switch them to v4 🤦

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked all of them locally, all worked with v3!


const Table = z.enum(['orders', 'customers', 'products']);
const Column = z.enum([
Expand Down
2 changes: 1 addition & 1 deletion examples/responses/structured-outputs-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { OpenAI } from 'openai';
import { zodResponsesFunction } from 'openai/helpers/zod';
import { z } from 'zod/v3';
import { z } from 'zod/v4'; // Also works for 'zod/v3'

const Table = z.enum(['orders', 'customers', 'products']);
const Column = z.enum([
Expand Down
2 changes: 1 addition & 1 deletion examples/responses/structured-outputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { OpenAI } from 'openai';
import { zodTextFormat } from 'openai/helpers/zod';
import { z } from 'zod/v3';
import { z } from 'zod/v4'; // Also works for 'zod/v3'

const Step = z.object({
explanation: z.string(),
Expand Down
2 changes: 1 addition & 1 deletion examples/tool-call-helpers-zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import OpenAI from 'openai';
import { zodFunction } from 'openai/helpers/zod';
import { z } from 'zod/v3';
import { z } from 'zod/v4'; // Also works for 'zod/v3'

// gets API Key from environment variable OPENAI_API_KEY
const openai = new OpenAI();
Expand Down
2 changes: 1 addition & 1 deletion examples/ui-generation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import OpenAI from 'openai';
import { z } from 'zod/v3';
import { z } from 'zod/v4'; // Also works for 'zod/v3'
import { zodResponseFormat } from 'openai/helpers/zod';

const openai = new OpenAI();
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@types/node": "^20.17.6",
"@typescript-eslint/eslint-plugin": "8.31.1",
"@typescript-eslint/parser": "8.31.1",
"deep-object-diff": "^1.1.9",
"eslint": "^9.20.1",
"eslint-plugin-prettier": "^5.4.1",
"eslint-plugin-unused-imports": "^4.1.4",
Expand Down
58 changes: 42 additions & 16 deletions src/helpers/zod.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ResponseFormatJSONSchema } from '../resources/index';
import type { infer as zodInfer, ZodType } from 'zod/v3';
import { z as z3 } from 'zod/v3';
import { z as z4 } from 'zod/v4';
import {
AutoParseableResponseFormat,
AutoParseableTextFormat,
Expand All @@ -11,8 +12,15 @@ 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<string, unknown> {
type InferZodType<T> =
T extends z4.ZodType ? z4.infer<T>
: T extends z3.ZodType ? z3.infer<T>
: never;

function zodV3ToJsonSchema(schema: z3.ZodType, options: { name: string }): Record<string, unknown> {
return _zodToJsonSchema(schema, {
openaiStrictMode: true,
name: options.name,
Expand All @@ -22,6 +30,18 @@ function zodToJsonSchema(schema: ZodType, options: { name: string }): Record<str
});
}

function zodV4ToJsonSchema(schema: z4.ZodType): Record<string, unknown> {
return toStrictJsonSchema(
z4.toJSONSchema(schema, {
target: 'draft-7',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just curious how we picked this value?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've lost the context here a little bit, but looking at it again, it seems like we shouldn't set the concrete target. I'll remove it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I think I did that because the current vendored version that parses Zod schema to JSON Schema is using draft-07 by default

}) as JSONSchema,
) as Record<string, unknown>;
}

function isZodV4(zodObject: z3.ZodType | z4.ZodType): zodObject is z4.ZodType {
return '_zod' in zodObject;
}

/**
* Creates a chat completion `JSONSchema` response format object from
* the given Zod schema.
Expand Down Expand Up @@ -59,37 +79,37 @@ function zodToJsonSchema(schema: ZodType, options: { name: string }): Record<str
* 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<ZodInput extends ZodType>(
export function zodResponseFormat<ZodInput extends z3.ZodType | z4.ZodType>(
zodObject: ZodInput,
name: string,
props?: Omit<ResponseFormatJSONSchema.JSONSchema, 'schema' | 'strict' | 'name'>,
): AutoParseableResponseFormat<zodInfer<ZodInput>> {
): AutoParseableResponseFormat<InferZodType<ZodInput>> {
return makeParseableResponseFormat(
{
type: 'json_schema',
json_schema: {
...props,
name,
strict: true,
schema: zodToJsonSchema(zodObject, { name }),
schema: isZodV4(zodObject) ? zodV4ToJsonSchema(zodObject) : zodV3ToJsonSchema(zodObject, { name }),
},
},
(content) => zodObject.parse(JSON.parse(content)),
);
}

export function zodTextFormat<ZodInput extends ZodType>(
export function zodTextFormat<ZodInput extends z3.ZodType | z4.ZodType>(
zodObject: ZodInput,
name: string,
props?: Omit<ResponseFormatTextJSONSchemaConfig, 'schema' | 'type' | 'strict' | 'name'>,
): AutoParseableTextFormat<zodInfer<ZodInput>> {
): AutoParseableTextFormat<InferZodType<ZodInput>> {
return makeParseableTextFormat(
{
type: 'json_schema',
...props,
name,
strict: true,
schema: zodToJsonSchema(zodObject, { name }),
schema: isZodV4(zodObject) ? zodV4ToJsonSchema(zodObject) : zodV3ToJsonSchema(zodObject, { name }),
},
(content) => zodObject.parse(JSON.parse(content)),
);
Expand All @@ -100,23 +120,26 @@ export function zodTextFormat<ZodInput extends ZodType>(
* automatically by the chat completion `.runTools()` method or automatically
* parsed by `.parse()` / `.stream()`.
*/
export function zodFunction<Parameters extends ZodType>(options: {
export function zodFunction<Parameters extends z3.ZodType | z4.ZodType>(options: {
name: string;
parameters: Parameters;
function?: ((args: zodInfer<Parameters>) => unknown | Promise<unknown>) | undefined;
function?: ((args: InferZodType<Parameters>) => unknown | Promise<unknown>) | undefined;
description?: string | undefined;
}): AutoParseableTool<{
arguments: Parameters;
name: string;
function: (args: zodInfer<Parameters>) => unknown;
function: (args: InferZodType<Parameters>) => unknown;
}> {
// @ts-expect-error TODO
return makeParseableTool<any>(
{
type: 'function',
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),
},
Expand All @@ -128,21 +151,24 @@ export function zodFunction<Parameters extends ZodType>(options: {
);
}

export function zodResponsesFunction<Parameters extends ZodType>(options: {
export function zodResponsesFunction<Parameters extends z3.ZodType | z4.ZodType>(options: {
name: string;
parameters: Parameters;
function?: ((args: zodInfer<Parameters>) => unknown | Promise<unknown>) | undefined;
function?: ((args: InferZodType<Parameters>) => unknown | Promise<unknown>) | undefined;
description?: string | undefined;
}): AutoParseableResponseTool<{
arguments: Parameters;
name: string;
function: (args: zodInfer<Parameters>) => unknown;
function: (args: InferZodType<Parameters>) => unknown;
}> {
return makeParseableResponseTool<any>(
{
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),
},
Expand Down
24 changes: 24 additions & 0 deletions src/lib/jsonschema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Loading
Loading