Skip to content
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'

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> {
return toJSONSchema(schema, { target: 'draft-7' }) as Record<string, unknown>;
}

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 },
])('zodResponseFormat (Zod $version)', ({ version, z }) => {
Comment on lines +5 to +8
Copy link
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
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
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