Skip to content

Commit

Permalink
fix(zod): correctly add $ref definitions for transformed schemas (#1065)
Browse files Browse the repository at this point in the history
  • Loading branch information
RobertCraigie authored and stainless-app[bot] committed Sep 13, 2024
1 parent 720a843 commit 908ae61
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 5 deletions.
11 changes: 8 additions & 3 deletions src/_vendor/zod-to-json-schema/parseDef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export function parseDef(

refs.seen.set(def, newItem);

const jsonSchema = selectParser(def, (def as any).typeName, refs);
const jsonSchema = selectParser(def, (def as any).typeName, refs, forceResolution);

if (jsonSchema) {
addMeta(def, refs, jsonSchema);
Expand Down Expand Up @@ -166,7 +166,12 @@ const getRelativePath = (pathA: string[], pathB: string[]) => {
return [(pathA.length - i).toString(), ...pathB.slice(i)].join('/');
};

const selectParser = (def: any, typeName: ZodFirstPartyTypeKind, refs: Refs): JsonSchema7Type | undefined => {
const selectParser = (
def: any,
typeName: ZodFirstPartyTypeKind,
refs: Refs,
forceResolution: boolean,
): JsonSchema7Type | undefined => {
switch (typeName) {
case ZodFirstPartyTypeKind.ZodString:
return parseStringDef(def, refs);
Expand Down Expand Up @@ -217,7 +222,7 @@ const selectParser = (def: any, typeName: ZodFirstPartyTypeKind, refs: Refs): Js
case ZodFirstPartyTypeKind.ZodNever:
return parseNeverDef();
case ZodFirstPartyTypeKind.ZodEffects:
return parseEffectsDef(def, refs);
return parseEffectsDef(def, refs, forceResolution);
case ZodFirstPartyTypeKind.ZodAny:
return parseAnyDef();
case ZodFirstPartyTypeKind.ZodUnknown:
Expand Down
8 changes: 6 additions & 2 deletions src/_vendor/zod-to-json-schema/parsers/effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { ZodEffectsDef } from 'zod';
import { JsonSchema7Type, parseDef } from '../parseDef';
import { Refs } from '../Refs';

export function parseEffectsDef(_def: ZodEffectsDef, refs: Refs): JsonSchema7Type | undefined {
return refs.effectStrategy === 'input' ? parseDef(_def.schema._def, refs) : {};
export function parseEffectsDef(
_def: ZodEffectsDef,
refs: Refs,
forceResolution: boolean,
): JsonSchema7Type | undefined {
return refs.effectStrategy === 'input' ? parseDef(_def.schema._def, refs, forceResolution) : {};
}
31 changes: 31 additions & 0 deletions tests/lib/__snapshots__/parser.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,37 @@ exports[`.parse() zod recursive schema extraction 2`] = `
"
`;

exports[`.parse() zod ref schemas with \`.transform()\` 2`] = `
"{
"id": "chatcmpl-A6zyLEtubMlUvGplOmr92S0mK0kiG",
"object": "chat.completion",
"created": 1726231553,
"model": "gpt-4o-2024-08-06",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "{\\"first\\":{\\"baz\\":true},\\"second\\":{\\"baz\\":false}}",
"refusal": null
},
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 167,
"completion_tokens": 13,
"total_tokens": 180,
"completion_tokens_details": {
"reasoning_tokens": 0
}
},
"system_fingerprint": "fp_143bb8492c"
}
"
`;

exports[`.parse() zod top-level recursive schemas 1`] = `
"{
"id": "chatcmpl-9uLhw79ArBF4KsQQOlsoE68m6vh6v",
Expand Down
114 changes: 114 additions & 0 deletions tests/lib/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -951,5 +951,119 @@ describe('.parse()', () => {
}
`);
});

test('ref schemas with `.transform()`', async () => {
const Inner = z.object({
baz: z.boolean().transform((v) => v ?? true),
});

const Outer = z.object({
first: Inner,
second: Inner,
});

expect(zodResponseFormat(Outer, 'data').json_schema.schema).toMatchInlineSnapshot(`
{
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"definitions": {
"data": {
"additionalProperties": false,
"properties": {
"first": {
"additionalProperties": false,
"properties": {
"baz": {
"type": "boolean",
},
},
"required": [
"baz",
],
"type": "object",
},
"second": {
"$ref": "#/definitions/data_properties_first",
},
},
"required": [
"first",
"second",
],
"type": "object",
},
"data_properties_first": {
"additionalProperties": false,
"properties": {
"baz": {
"$ref": "#/definitions/data_properties_first_properties_baz",
},
},
"required": [
"baz",
],
"type": "object",
},
"data_properties_first_properties_baz": {
"type": "boolean",
},
},
"properties": {
"first": {
"additionalProperties": false,
"properties": {
"baz": {
"type": "boolean",
},
},
"required": [
"baz",
],
"type": "object",
},
"second": {
"$ref": "#/definitions/data_properties_first",
},
},
"required": [
"first",
"second",
],
"type": "object",
}
`);

const completion = await makeSnapshotRequest(
(openai) =>
openai.beta.chat.completions.parse({
model: 'gpt-4o-2024-08-06',
messages: [
{
role: 'user',
content: 'can you generate fake data matching the given response format?',
},
],
response_format: zodResponseFormat(Outer, 'fakeData'),
}),
2,
);

expect(completion.choices[0]?.message).toMatchInlineSnapshot(`
{
"content": "{"first":{"baz":true},"second":{"baz":false}}",
"parsed": {
"first": {
"baz": true,
},
"second": {
"baz": false,
},
},
"refusal": null,
"role": "assistant",
"tool_calls": [],
}
`);
});
});
});

0 comments on commit 908ae61

Please sign in to comment.