Skip to content

Commit f058f7a

Browse files
committed
Support Zod v3 and v4.
1 parent 96a6d75 commit f058f7a

File tree

8 files changed

+2481
-2244
lines changed

8 files changed

+2481
-2244
lines changed

core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"@modelcontextprotocol/sdk": "^1.24.0",
4444
"google-auth-library": "^10.3.0",
4545
"lodash-es": "^4.17.22",
46-
"zod": "3.25.76"
46+
"zod": "^4.2.1"
4747
},
4848
"devDependencies": {
4949
"@types/lodash-es": "^4.17.12",

core/src/tools/function_tool.ts

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,33 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import {FunctionDeclaration, Schema, Type} from '@google/genai';
8-
import {
9-
type infer as zInfer,
10-
ZodObject,
11-
type ZodRawShape,
12-
} from 'zod';
7+
import { FunctionDeclaration, Schema, Type } from '@google/genai';
8+
import { z as z3 } from 'zod/v3';
9+
import { z as z4 } from 'zod/v4';
1310

14-
import {isZodObject, zodObjectToSchema} from '../utils/simple_zod_to_json.js';
11+
import { isZodObject, zodObjectToSchema } from '../utils/simple_zod_to_json.js';
1512

16-
import {BaseTool, RunAsyncToolRequest} from './base_tool.js';
17-
import {ToolContext} from './tool_context.js';
13+
import { BaseTool, RunAsyncToolRequest } from './base_tool.js';
14+
import { ToolContext } from './tool_context.js';
1815

1916
/**
2017
* Input parameters of the function tool.
2118
*/
22-
export type ToolInputParameters =
23-
| undefined
24-
| ZodObject<ZodRawShape>
25-
| Schema;
19+
export type ToolInputParameters = | undefined | z3.ZodObject<z3.ZodRawShape> | z4.ZodObject | Schema;
20+
21+
type ZodObject<T extends Record<string, any>> = z3.ZodObject<z3.ZodRawShape> | z4.ZodObject<T>;
2622

2723
/*
2824
* The arguments of the function tool.
2925
*/
3026
export type ToolExecuteArgument<TParameters extends ToolInputParameters> =
31-
TParameters extends ZodObject<infer T, infer U, infer V>
32-
? zInfer<ZodObject<T, U, V>>
33-
: TParameters extends Schema
34-
? unknown
35-
: string;
27+
TParameters extends z3.ZodObject<infer T, infer U, infer V>
28+
? z3.infer<z3.ZodObject<T, U, V>>
29+
: TParameters extends z4.ZodObject<infer T>
30+
? z4.infer<z4.ZodObject<T>>
31+
: TParameters extends Schema
32+
? unknown
33+
: string;
3634

3735
/*
3836
* The function to execute by the tool.
@@ -65,7 +63,7 @@ export type ToolOptions<
6563
function toSchema<TParameters extends ToolInputParameters>(
6664
parameters: TParameters): Schema {
6765
if (parameters === undefined) {
68-
return {type: Type.OBJECT, properties: {}};
66+
return { type: Type.OBJECT, properties: {} };
6967
}
7068

7169
if (isZodObject(parameters)) {
@@ -120,7 +118,7 @@ export class FunctionTool<
120118
override async runAsync(req: RunAsyncToolRequest): Promise<unknown> {
121119
try {
122120
let validatedArgs: unknown = req.args;
123-
if (this.parameters instanceof ZodObject) {
121+
if (isZodObject(this.parameters)) {
124122
validatedArgs = this.parameters.parse(req.args);
125123
}
126124
return await this.execute(

core/src/utils/gemini_schema_util.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {z} from 'zod';
99

1010
const MCPToolSchema = z.object({
1111
type: z.literal('object'),
12-
properties: z.record(z.unknown()).optional(),
12+
properties: z.record(z.string(), z.unknown()).optional(),
1313
required: z.string().array().optional(),
1414
});
1515
type MCPToolSchema = z.infer<typeof MCPToolSchema>;

core/src/utils/simple_zod_to_json.ts

Lines changed: 107 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,57 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import {Schema, Type} from '@google/genai';
8-
import {z, ZodObject, ZodTypeAny} from 'zod';
7+
import { Schema, Type } from '@google/genai';
8+
import { z as z3 } from 'zod/v3';
9+
import { z as z4, toJSONSchema as toJSONSchemaV4 } from 'zod/v4';
10+
11+
type ZodSchema<T = any> = z3.ZodType<T> | z4.ZodType<T>;
12+
13+
type SchemaLike = ZodSchema | Schema;
14+
15+
function isZodSchema(obj: unknown): obj is ZodSchema {
16+
return (
17+
obj !== null &&
18+
typeof obj === "object" &&
19+
"parse" in obj &&
20+
typeof (obj as { parse: unknown }).parse === "function" &&
21+
"safeParse" in obj &&
22+
typeof (obj as { safeParse: unknown }).safeParse === "function"
23+
);
24+
}
25+
26+
function isZodV3Schema(obj: unknown): obj is z3.ZodTypeAny {
27+
return isZodSchema(obj) && !("_zod" in obj);
28+
}
29+
30+
function isZodV4Schema(obj: unknown): obj is z4.ZodType {
31+
return isZodSchema(obj) && "_zod" in obj;
32+
}
33+
34+
function getZodTypeName(schema: z3.ZodTypeAny | z4.ZodType): string | undefined {
35+
const schemaAny = schema as any;
36+
37+
if (schemaAny._def?.typeName) {
38+
return schemaAny._def.typeName;
39+
}
40+
41+
const zod4Type = schemaAny._def?.type;
42+
if (typeof zod4Type === 'string' && zod4Type) {
43+
return 'Zod' + zod4Type.charAt(0).toUpperCase() + zod4Type.slice(1);
44+
}
45+
46+
return undefined;
47+
}
948

1049
/**
11-
* Returns true if the given object is a V3 ZodObject.
50+
* Returns true if the given object is a ZodObject (supports both Zod v3 and v4).
1251
*/
13-
export function isZodObject(obj: unknown): obj is ZodObject<any> {
14-
return (
15-
obj !== null && typeof obj === 'object' &&
16-
(obj as any)._def?.typeName === 'ZodObject');
52+
export function isZodObject(obj: unknown): obj is z3.ZodObject<any> | z4.ZodObject<any> {
53+
return isZodSchema(obj) && getZodTypeName(obj) === 'ZodObject';
1754
}
1855

1956
// TODO(b/425992518): consider conversion to FunctionDeclaration directly.
20-
21-
function parseZodType(zodType: ZodTypeAny): Schema|undefined {
57+
function parseZodV3Type(zodType: z3.ZodTypeAny): Schema | undefined {
2258
const def = zodType._def;
2359
if (!def) {
2460
return {};
@@ -35,7 +71,7 @@ function parseZodType(zodType: ZodTypeAny): Schema|undefined {
3571
};
3672

3773
switch (def.typeName) {
38-
case z.ZodFirstPartyTypeKind.ZodString:
74+
case z3.ZodFirstPartyTypeKind.ZodString:
3975
result.type = Type.STRING;
4076
for (const check of def.checks || []) {
4177
if (check.kind === 'min')
@@ -53,7 +89,7 @@ function parseZodType(zodType: ZodTypeAny): Schema|undefined {
5389
}
5490
return returnResult(result);
5591

56-
case z.ZodFirstPartyTypeKind.ZodNumber:
92+
case z3.ZodFirstPartyTypeKind.ZodNumber:
5793
result.type = Type.NUMBER;
5894
for (const check of def.checks || []) {
5995
if (check.kind === 'min')
@@ -65,23 +101,23 @@ function parseZodType(zodType: ZodTypeAny): Schema|undefined {
65101
}
66102
return returnResult(result);
67103

68-
case z.ZodFirstPartyTypeKind.ZodBoolean:
104+
case z3.ZodFirstPartyTypeKind.ZodBoolean:
69105
result.type = Type.BOOLEAN;
70106
return returnResult(result);
71107

72-
case z.ZodFirstPartyTypeKind.ZodArray:
108+
case z3.ZodFirstPartyTypeKind.ZodArray:
73109
result.type = Type.ARRAY;
74-
result.items = parseZodType(def.type);
110+
result.items = parseZodV3Type(def.type);
75111
if (def.minLength) result.minItems = def.minLength.value.toString();
76112
if (def.maxLength) result.maxItems = def.maxLength.value.toString();
77113
return returnResult(result);
78114

79-
case z.ZodFirstPartyTypeKind.ZodObject: {
80-
const nestedSchema = zodObjectToSchema(zodType as ZodObject<any>);
115+
case z3.ZodFirstPartyTypeKind.ZodObject: {
116+
const nestedSchema = zodObjectToSchema(zodType as z3.ZodObject<any>);
81117
return nestedSchema as Schema;
82118
}
83119

84-
case z.ZodFirstPartyTypeKind.ZodLiteral:
120+
case z3.ZodFirstPartyTypeKind.ZodLiteral:
85121
const literalType = typeof def.value;
86122
result.enum = [def.value.toString()];
87123

@@ -99,71 +135,67 @@ function parseZodType(zodType: ZodTypeAny): Schema|undefined {
99135

100136
return returnResult(result);
101137

102-
case z.ZodFirstPartyTypeKind.ZodEnum:
138+
case z3.ZodFirstPartyTypeKind.ZodEnum:
103139
result.type = Type.STRING;
104140
result.enum = def.values;
105141
return returnResult(result);
106142

107-
case z.ZodFirstPartyTypeKind.ZodNativeEnum:
143+
case z3.ZodFirstPartyTypeKind.ZodNativeEnum:
108144
result.type = Type.STRING;
109145
result.enum = Object.values(def.values);
110146
return returnResult(result);
111147

112-
case z.ZodFirstPartyTypeKind.ZodUnion:
113-
result.anyOf = def.options.map(parseZodType);
148+
case z3.ZodFirstPartyTypeKind.ZodUnion:
149+
result.anyOf = def.options.map(parseZodV3Type);
114150
return returnResult(result);
115151

116-
case z.ZodFirstPartyTypeKind.ZodOptional:
117-
return parseZodType(def.innerType);
118-
case z.ZodFirstPartyTypeKind.ZodNullable:
119-
const nullableInner = parseZodType(def.innerType);
152+
case z3.ZodFirstPartyTypeKind.ZodOptional:
153+
return parseZodV3Type(def.innerType);
154+
case z3.ZodFirstPartyTypeKind.ZodNullable:
155+
const nullableInner = parseZodV3Type(def.innerType);
120156
return nullableInner ?
121-
returnResult({
122-
anyOf: [nullableInner, {type: Type.NULL}],
123-
...(description && {description})
124-
}) :
125-
returnResult({type: Type.NULL, ...(description && {description})});
126-
case z.ZodFirstPartyTypeKind.ZodDefault:
127-
const defaultInner = parseZodType(def.innerType);
157+
returnResult({
158+
anyOf: [nullableInner, { type: Type.NULL }],
159+
...(description && { description })
160+
}) :
161+
returnResult({ type: Type.NULL, ...(description && { description }) });
162+
case z3.ZodFirstPartyTypeKind.ZodDefault:
163+
const defaultInner = parseZodV3Type(def.innerType);
128164
if (defaultInner) defaultInner.default = def.defaultValue();
129165
return defaultInner;
130-
case z.ZodFirstPartyTypeKind.ZodBranded:
131-
return parseZodType(def.type);
132-
case z.ZodFirstPartyTypeKind.ZodReadonly:
133-
return parseZodType(def.innerType);
134-
case z.ZodFirstPartyTypeKind.ZodNull:
166+
case z3.ZodFirstPartyTypeKind.ZodBranded:
167+
return parseZodV3Type(def.type);
168+
case z3.ZodFirstPartyTypeKind.ZodReadonly:
169+
return parseZodV3Type(def.innerType);
170+
case z3.ZodFirstPartyTypeKind.ZodNull:
135171
result.type = Type.NULL;
136172
return returnResult(result);
137-
case z.ZodFirstPartyTypeKind.ZodAny:
138-
case z.ZodFirstPartyTypeKind.ZodUnknown:
139-
return returnResult({...(description && {description})});
173+
case z3.ZodFirstPartyTypeKind.ZodAny:
174+
case z3.ZodFirstPartyTypeKind.ZodUnknown:
175+
return returnResult({ ...(description && { description }) });
140176
default:
141177
throw new Error(`Unsupported Zod type: ${def.typeName}`);
142178
}
143179
}
144180

145-
export function zodObjectToSchema(schema: ZodObject<any>): Schema {
146-
if (schema._def.typeName !== z.ZodFirstPartyTypeKind.ZodObject) {
147-
throw new Error('Expected a ZodObject');
148-
}
149-
181+
function toJsonSchemaZ3(schema: z3.ZodObject<z3.ZodRawShape>): Schema {
150182
const shape = schema.shape;
151183
const properties: Record<string, Schema> = {};
152184
const required: string[] = [];
153185

154186
for (const key in shape) {
155187
const fieldSchema = shape[key];
156-
const parsedField = parseZodType(fieldSchema);
188+
const parsedField = parseZodV3Type(fieldSchema);
157189
if (parsedField) {
158190
properties[key] = parsedField;
159191
}
160192

161193
let currentSchema = fieldSchema;
162194
let isOptional = false;
163195
while (currentSchema._def.typeName ===
164-
z.ZodFirstPartyTypeKind.ZodOptional ||
165-
currentSchema._def.typeName === z.ZodFirstPartyTypeKind.ZodDefault) {
166-
isOptional = true;
196+
z3.ZodFirstPartyTypeKind.ZodOptional ||
197+
currentSchema._def.typeName === z3.ZodFirstPartyTypeKind.ZodDefault) {
198+
isOptional = true;
167199
currentSchema = currentSchema._def.innerType;
168200
}
169201
if (!isOptional) {
@@ -172,16 +204,38 @@ export function zodObjectToSchema(schema: ZodObject<any>): Schema {
172204
}
173205

174206
const catchall = schema._def.catchall;
175-
let additionalProperties: boolean|Schema = false;
176-
if (catchall && catchall._def.typeName !== z.ZodFirstPartyTypeKind.ZodNever) {
177-
additionalProperties = parseZodType(catchall) || true;
207+
let additionalProperties: boolean | Schema = false;
208+
if (catchall && catchall._def.typeName !== z3.ZodFirstPartyTypeKind.ZodNever) {
209+
additionalProperties = parseZodV3Type(catchall) || true;
178210
} else {
179211
additionalProperties = schema._def.unknownKeys === 'passthrough';
180212
}
181213
return {
182214
type: Type.OBJECT,
183215
properties,
184216
required: required.length > 0 ? required : [],
185-
...(schema._def.description ? {description: schema._def.description} : {}),
217+
...(schema._def.description ? { description: schema._def.description } : {}),
186218
};
187219
}
220+
221+
export function zodObjectToSchema(schema: z3.ZodObject<z3.ZodRawShape> | z4.ZodObject<z4.ZodRawShape>): Schema {
222+
if (!isZodObject(schema)) {
223+
throw new Error('Expected a Zod Object');
224+
}
225+
226+
if (isZodV4Schema(schema)) {
227+
return toJSONSchemaV4(schema, {
228+
target: 'openapi-3.0', override(ctx) {
229+
if (ctx.jsonSchema.additionalProperties !== undefined) {
230+
delete ctx.jsonSchema.additionalProperties;
231+
}
232+
},
233+
}) as Schema;
234+
}
235+
236+
if (isZodV3Schema(schema)) {
237+
return toJsonSchemaZ3(schema);
238+
}
239+
240+
throw new Error('Unsupported Zod schema version.');
241+
}

0 commit comments

Comments
 (0)