Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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.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/v3'; // Also works for 'zod/v4'
Comment thread
RobertCraigie marked this conversation as resolved.
Outdated

const Step = z.object({
explanation: z.string(),
Expand Down
10 changes: 7 additions & 3 deletions src/helpers/zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ function zodToJsonSchema(schema: ZodType, options: { name: string }): Record<str
});
}

function nativeToJsonSchema(schema: ZodTypeV4): Record<string, unknown> {
Comment thread
RobertCraigie marked this conversation as resolved.
Outdated
return toJSONSchema(schema, { target: 'draft-7' }) as Record<string, unknown>;
Comment thread
RobertCraigie marked this conversation as resolved.
Outdated
}

function isZodV4(zodObject: ZodType | ZodTypeV4): zodObject is ZodTypeV4 {
return '_zod' in zodObject;
}
Expand Down Expand Up @@ -90,7 +94,7 @@ export function zodResponseFormat<ZodInput extends ZodType | ZodTypeV4>(
strict: true,
schema:
isZodV4(zodObject) ?
(toStrictJsonSchema(toJSONSchema(zodObject) as JSONSchema) as Record<string, unknown>)
(toStrictJsonSchema(nativeToJsonSchema(zodObject)) as Record<string, unknown>)
: zodToJsonSchema(zodObject, { name }),
},
},
Expand Down Expand Up @@ -121,7 +125,7 @@ export function zodTextFormat<ZodInput extends ZodType | ZodTypeV4>(
strict: true,
schema:
isZodV4(zodObject) ?
(toStrictJsonSchema(toJSONSchema(zodObject) as JSONSchema) as Record<string, unknown>)
(toStrictJsonSchema(nativeToJsonSchema(zodObject)) as Record<string, unknown>)
: zodToJsonSchema(zodObject, { name }),
},
(content) => zodObject.parse(JSON.parse(content)),
Expand Down Expand Up @@ -166,7 +170,7 @@ export function zodFunction<Parameters extends ZodType | ZodTypeV4>(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),
Expand Down
12 changes: 8 additions & 4 deletions tests/helpers/zod.test.ts
Original file line number Diff line number Diff line change
@@ -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 },
Comment thread
RobertCraigie marked this conversation as resolved.
Outdated
])('zodResponseFormat (Zod $version)', ({ version, z }) => {
Comment on lines +5 to +8
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

👌 this is a cool way to test things

it('does the thing', () => {
expect(
zodResponseFormat(
Expand Down Expand Up @@ -286,7 +290,7 @@ describe('zodResponseFormat', () => {
`);
});

it('throws error on optional fields', () => {
(version === 'v4' ? it.skip : it)('throws error on optional fields', () => {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

as we transform the schema, we actually add missing fields in required property, as we do in python

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

hmm I don't think this is quite right, in Python it's fine because the property being omitted or explicitly set to null results in the same type, None, but in TS it's different.

so if we add properties that are .optional() to the required array, then the API will send them back as null, which breaks the type promise because it'd be typed as property?: string instead of property?: string | null or property: string | null.

the equivalent behaviour here for python would be to only add properties to required when they're both .optional() and .nullable() which is why we throw the current error.

expect(() =>
zodResponseFormat(
z.object({
Expand All @@ -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({
Expand Down
Loading