Skip to content

Commit 908ae61

Browse files
RobertCraigiestainless-app[bot]
authored andcommitted
fix(zod): correctly add $ref definitions for transformed schemas (#1065)
1 parent 720a843 commit 908ae61

File tree

4 files changed

+159
-5
lines changed

4 files changed

+159
-5
lines changed

src/_vendor/zod-to-json-schema/parseDef.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export function parseDef(
9999

100100
refs.seen.set(def, newItem);
101101

102-
const jsonSchema = selectParser(def, (def as any).typeName, refs);
102+
const jsonSchema = selectParser(def, (def as any).typeName, refs, forceResolution);
103103

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

169-
const selectParser = (def: any, typeName: ZodFirstPartyTypeKind, refs: Refs): JsonSchema7Type | undefined => {
169+
const selectParser = (
170+
def: any,
171+
typeName: ZodFirstPartyTypeKind,
172+
refs: Refs,
173+
forceResolution: boolean,
174+
): JsonSchema7Type | undefined => {
170175
switch (typeName) {
171176
case ZodFirstPartyTypeKind.ZodString:
172177
return parseStringDef(def, refs);
@@ -217,7 +222,7 @@ const selectParser = (def: any, typeName: ZodFirstPartyTypeKind, refs: Refs): Js
217222
case ZodFirstPartyTypeKind.ZodNever:
218223
return parseNeverDef();
219224
case ZodFirstPartyTypeKind.ZodEffects:
220-
return parseEffectsDef(def, refs);
225+
return parseEffectsDef(def, refs, forceResolution);
221226
case ZodFirstPartyTypeKind.ZodAny:
222227
return parseAnyDef();
223228
case ZodFirstPartyTypeKind.ZodUnknown:

src/_vendor/zod-to-json-schema/parsers/effects.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { ZodEffectsDef } from 'zod';
22
import { JsonSchema7Type, parseDef } from '../parseDef';
33
import { Refs } from '../Refs';
44

5-
export function parseEffectsDef(_def: ZodEffectsDef, refs: Refs): JsonSchema7Type | undefined {
6-
return refs.effectStrategy === 'input' ? parseDef(_def.schema._def, refs) : {};
5+
export function parseEffectsDef(
6+
_def: ZodEffectsDef,
7+
refs: Refs,
8+
forceResolution: boolean,
9+
): JsonSchema7Type | undefined {
10+
return refs.effectStrategy === 'input' ? parseDef(_def.schema._def, refs, forceResolution) : {};
711
}

tests/lib/__snapshots__/parser.test.ts.snap

+31
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,37 @@ exports[`.parse() zod recursive schema extraction 2`] = `
112112
"
113113
`;
114114

115+
exports[`.parse() zod ref schemas with \`.transform()\` 2`] = `
116+
"{
117+
"id": "chatcmpl-A6zyLEtubMlUvGplOmr92S0mK0kiG",
118+
"object": "chat.completion",
119+
"created": 1726231553,
120+
"model": "gpt-4o-2024-08-06",
121+
"choices": [
122+
{
123+
"index": 0,
124+
"message": {
125+
"role": "assistant",
126+
"content": "{\\"first\\":{\\"baz\\":true},\\"second\\":{\\"baz\\":false}}",
127+
"refusal": null
128+
},
129+
"logprobs": null,
130+
"finish_reason": "stop"
131+
}
132+
],
133+
"usage": {
134+
"prompt_tokens": 167,
135+
"completion_tokens": 13,
136+
"total_tokens": 180,
137+
"completion_tokens_details": {
138+
"reasoning_tokens": 0
139+
}
140+
},
141+
"system_fingerprint": "fp_143bb8492c"
142+
}
143+
"
144+
`;
145+
115146
exports[`.parse() zod top-level recursive schemas 1`] = `
116147
"{
117148
"id": "chatcmpl-9uLhw79ArBF4KsQQOlsoE68m6vh6v",

tests/lib/parser.test.ts

+114
Original file line numberDiff line numberDiff line change
@@ -951,5 +951,119 @@ describe('.parse()', () => {
951951
}
952952
`);
953953
});
954+
955+
test('ref schemas with `.transform()`', async () => {
956+
const Inner = z.object({
957+
baz: z.boolean().transform((v) => v ?? true),
958+
});
959+
960+
const Outer = z.object({
961+
first: Inner,
962+
second: Inner,
963+
});
964+
965+
expect(zodResponseFormat(Outer, 'data').json_schema.schema).toMatchInlineSnapshot(`
966+
{
967+
"$schema": "http://json-schema.org/draft-07/schema#",
968+
"additionalProperties": false,
969+
"definitions": {
970+
"data": {
971+
"additionalProperties": false,
972+
"properties": {
973+
"first": {
974+
"additionalProperties": false,
975+
"properties": {
976+
"baz": {
977+
"type": "boolean",
978+
},
979+
},
980+
"required": [
981+
"baz",
982+
],
983+
"type": "object",
984+
},
985+
"second": {
986+
"$ref": "#/definitions/data_properties_first",
987+
},
988+
},
989+
"required": [
990+
"first",
991+
"second",
992+
],
993+
"type": "object",
994+
},
995+
"data_properties_first": {
996+
"additionalProperties": false,
997+
"properties": {
998+
"baz": {
999+
"$ref": "#/definitions/data_properties_first_properties_baz",
1000+
},
1001+
},
1002+
"required": [
1003+
"baz",
1004+
],
1005+
"type": "object",
1006+
},
1007+
"data_properties_first_properties_baz": {
1008+
"type": "boolean",
1009+
},
1010+
},
1011+
"properties": {
1012+
"first": {
1013+
"additionalProperties": false,
1014+
"properties": {
1015+
"baz": {
1016+
"type": "boolean",
1017+
},
1018+
},
1019+
"required": [
1020+
"baz",
1021+
],
1022+
"type": "object",
1023+
},
1024+
"second": {
1025+
"$ref": "#/definitions/data_properties_first",
1026+
},
1027+
},
1028+
"required": [
1029+
"first",
1030+
"second",
1031+
],
1032+
"type": "object",
1033+
}
1034+
`);
1035+
1036+
const completion = await makeSnapshotRequest(
1037+
(openai) =>
1038+
openai.beta.chat.completions.parse({
1039+
model: 'gpt-4o-2024-08-06',
1040+
messages: [
1041+
{
1042+
role: 'user',
1043+
content: 'can you generate fake data matching the given response format?',
1044+
},
1045+
],
1046+
response_format: zodResponseFormat(Outer, 'fakeData'),
1047+
}),
1048+
2,
1049+
);
1050+
1051+
expect(completion.choices[0]?.message).toMatchInlineSnapshot(`
1052+
{
1053+
"content": "{"first":{"baz":true},"second":{"baz":false}}",
1054+
"parsed": {
1055+
"first": {
1056+
"baz": true,
1057+
},
1058+
"second": {
1059+
"baz": false,
1060+
},
1061+
},
1062+
"refusal": null,
1063+
"role": "assistant",
1064+
"tool_calls": [],
1065+
}
1066+
`);
1067+
});
9541068
});
9551069
});

0 commit comments

Comments
 (0)