diff --git a/langchain-core/src/utils/types/tests/zod.test.ts b/langchain-core/src/utils/types/tests/zod.test.ts index f1b7280c2494..423d1841d547 100644 --- a/langchain-core/src/utils/types/tests/zod.test.ts +++ b/langchain-core/src/utils/types/tests/zod.test.ts @@ -1527,6 +1527,96 @@ describe("Zod utility functions", () => { }); }); + it("should handle nested transforms in optional objects", () => { + // Create a schema where nested objects are transformed and outside is optional + const userSchema = z4.object({ + name: z4.string().transform((s) => s.toUpperCase()), + age: z4.number(), + }); + const inputSchema = z4.object({ + user: z4.optional(userSchema), + metadata: z4.string(), + }); + const result = interopZodTransformInputSchema(inputSchema, true); + expect(result).toBeInstanceOf(z4.ZodObject); + const resultShape = getInteropZodObjectShape(result as any); + expect(Object.keys(resultShape)).toEqual(["user", "metadata"]); + expect(resultShape.user).toBeInstanceOf(z4.ZodOptional); + const userInnerType = (resultShape.user as z4.ZodOptional).def.innerType; + expect(userInnerType).toBeInstanceOf(z4.ZodObject); + const userShape = getInteropZodObjectShape(userInnerType as any); + expect(Object.keys(userShape)).toEqual(["name", "age"]); + expect(userShape.name).toBeInstanceOf(z4.ZodString); + expect(userShape.age).toBeInstanceOf(z4.ZodNumber); + }); + + it("should handle nested transforms in optional arrays", () => { + // Create a schema where nested arrays are transformed and outside is optional + const userSchema = z4.object({ + name: z4.string().transform((s) => s.toUpperCase()), + age: z4.number(), + }); + const inputSchema = z4.object({ + users: z4.optional(z4.array(userSchema)), + metadata: z4.string(), + }); + const result = interopZodTransformInputSchema(inputSchema, true); + expect(result).toBeInstanceOf(z4.ZodObject); + const resultShape = getInteropZodObjectShape(result as any); + expect(Object.keys(resultShape)).toEqual(["users", "metadata"]); + expect(resultShape.users).toBeInstanceOf(z4.ZodOptional); + const userInnerType = (resultShape.users as z4.ZodOptional).def.innerType; + expect(userInnerType).toBeInstanceOf(z4.ZodArray); + const arrayElement = (userInnerType as z4.ZodArray)._zod.def.element; + expect(arrayElement).toBeInstanceOf(z4.ZodObject); + const usersShape = getInteropZodObjectShape(arrayElement as any); + expect(Object.keys(usersShape)).toEqual(["name", "age"]); + expect(usersShape.name).toBeInstanceOf(z4.ZodString); + expect(usersShape.age).toBeInstanceOf(z4.ZodNumber); + }); + + it("should handle transforms on optional objects", () => { + const userSchema = z4 + .object({ + name: z4.string(), + age: z4.number(), + }) + .transform((x) => x) + .optional(); + + const result = interopZodTransformInputSchema(userSchema, true); + expect(result).toBeInstanceOf(z4.ZodOptional); + const resultInnerType = (result as z4.ZodOptional).def.innerType; + expect(resultInnerType).toBeInstanceOf(z4.ZodObject); + const resultShape = getInteropZodObjectShape(resultInnerType as any); + expect(Object.keys(resultShape)).toEqual(["name", "age"]); + expect(resultShape.name).toBeInstanceOf(z4.ZodString); + expect(resultShape.age).toBeInstanceOf(z4.ZodNumber); + }); + + it("should handle transforms on optional arrays", () => { + const userSchema = z4 + .array( + z4.object({ + name: z4.string(), + age: z4.number(), + }) + ) + .transform((x) => x) + .optional(); + + const result = interopZodTransformInputSchema(userSchema, true); + expect(result).toBeInstanceOf(z4.ZodOptional); + const resultInnerType = (result as z4.ZodOptional).def.innerType; + expect(resultInnerType).toBeInstanceOf(z4.ZodArray); + const arrayElement = (resultInnerType as z4.ZodArray)._zod.def.element; + expect(arrayElement).toBeInstanceOf(z4.ZodObject); + const arrayElementShape = getInteropZodObjectShape(arrayElement as any); + expect(Object.keys(arrayElementShape)).toEqual(["name", "age"]); + expect(arrayElementShape.name).toBeInstanceOf(z4.ZodString); + expect(arrayElementShape.age).toBeInstanceOf(z4.ZodNumber); + }); + it("should throw error for non-schema values", () => { expect(() => interopZodTransformInputSchema(null as any)).toThrow(); expect(() => interopZodTransformInputSchema(undefined as any)).toThrow(); diff --git a/langchain-core/src/utils/types/zod.ts b/langchain-core/src/utils/types/zod.ts index ef21b65fe4ec..09fe02c2cb44 100644 --- a/langchain-core/src/utils/types/zod.ts +++ b/langchain-core/src/utils/types/zod.ts @@ -447,6 +447,26 @@ export function isZodArrayV4(obj: unknown): obj is z4.$ZodArray { return false; } +export function isZodOptionalV4(obj: unknown): obj is z4.$ZodOptional { + if (!isZodSchemaV4(obj)) return false; + // Zod v4 optional schemas have _zod.def.type === "optional" + if ( + typeof obj === "object" && + obj !== null && + "_zod" in obj && + typeof obj._zod === "object" && + obj._zod !== null && + "def" in obj._zod && + typeof obj._zod.def === "object" && + obj._zod.def !== null && + "type" in obj._zod.def && + obj._zod.def.type === "optional" + ) { + return true; + } + return false; +} + /** * Determines if the provided value is an InteropZodObject (Zod v3 or v4 object schema). * @@ -773,6 +793,17 @@ export function interopZodTransformInputSchema( element: elementSchema as z4.$ZodType, }); } + // Handle nested optional schemas + else if (isZodOptionalV4(outputSchema)) { + const innerType = interopZodTransformInputSchema( + outputSchema._zod.def.innerType, + recursive + ) as z4.$ZodType; + outputSchema = clone(outputSchema, { + ...outputSchema._zod.def, + innerType, + }); + } } const meta = globalRegistry.get(schema); if (meta) globalRegistry.add(outputSchema as z4.$ZodType, meta);