diff --git a/langchain-core/src/utils/json_schema.ts b/langchain-core/src/utils/json_schema.ts index 1162405d5f98..47c41c221e94 100644 --- a/langchain-core/src/utils/json_schema.ts +++ b/langchain-core/src/utils/json_schema.ts @@ -5,10 +5,8 @@ import { isZodSchemaV3, isZodSchemaV4, InteropZodType, - interopZodObjectStrict, - isZodObjectV4, - ZodObjectV4, - interopZodTransformInputSchema, + ZodTypeV4, + interopZodSanitizeSchema, } from "./types/zod.js"; export type JSONSchema = JsonSchema7Type; @@ -22,16 +20,8 @@ export { deepCompareStrict, Validator } from "@cfworker/json-schema"; */ export function toJsonSchema(schema: InteropZodType | JSONSchema): JSONSchema { if (isZodSchemaV4(schema)) { - const inputSchema = interopZodTransformInputSchema(schema, true); - if (isZodObjectV4(inputSchema)) { - const strictSchema = interopZodObjectStrict( - inputSchema, - true - ) as ZodObjectV4; - return toJSONSchema(strictSchema); - } else { - return toJSONSchema(schema); - } + const inputSchema = interopZodSanitizeSchema(schema, true); + return toJSONSchema(inputSchema as ZodTypeV4); } if (isZodSchemaV3(schema)) { return zodToJsonSchema(schema); diff --git a/langchain-core/src/utils/types/tests/zod.test.ts b/langchain-core/src/utils/types/tests/zod.test.ts index f1b7280c2494..c60bd858b2b9 100644 --- a/langchain-core/src/utils/types/tests/zod.test.ts +++ b/langchain-core/src/utils/types/tests/zod.test.ts @@ -1525,6 +1525,48 @@ describe("Zod utility functions", () => { expect(elementShape.name).toBeInstanceOf(z4.ZodString); expect(elementShape.age).toBeInstanceOf(z4.ZodNumber); }); + + it("should not mutate the original schema when object", () => { + const inputSchema = z4.object({ + user: z4.object({ + name: z4.string().transform((s) => s.toUpperCase()), + age: z4.number().transform((n) => n * 2), + }), + }); + + const result = interopZodTransformInputSchema(inputSchema, true); + + expect(result).not.toBe(inputSchema); + expect(inputSchema.shape.user.shape.name).toBeInstanceOf(z4.ZodPipe); + expect(inputSchema.shape.user.shape.age).toBeInstanceOf(z4.ZodPipe); + expect((result as z4.ZodObject).shape.user.shape.name).toBeInstanceOf( + z4.ZodString + ); + expect((result as z4.ZodObject).shape.user.shape.age).toBeInstanceOf( + z4.ZodNumber + ); + }); + + it("should not mutate the original schema when array", () => { + const inputSchema = z4.array( + z4.object({ + name: z4.string().transform((s) => s.toUpperCase()), + age: z4.number().transform((n) => n * 2), + }) + ); + + const result = interopZodTransformInputSchema(inputSchema, true); + + expect(result).not.toBe(inputSchema); + expect(inputSchema.element.shape.name).toBeInstanceOf(z4.ZodPipe); + expect(inputSchema.element.shape.age).toBeInstanceOf(z4.ZodPipe); + expect( + ((result as z4.ZodArray).element as z4.ZodObject).shape.name + ).toBeInstanceOf(z4.ZodString); + expect( + ((result as z4.ZodArray).element as z4.ZodObject).shape.age + ).toBeInstanceOf(z4.ZodNumber); + }); }); it("should throw error for non-schema values", () => { diff --git a/langchain-core/src/utils/types/zod.ts b/langchain-core/src/utils/types/zod.ts index ef21b65fe4ec..961e72ad7dd8 100644 --- a/langchain-core/src/utils/types/zod.ts +++ b/langchain-core/src/utils/types/zod.ts @@ -23,6 +23,8 @@ export type ZodObjectV3 = z3.ZodObject; export type ZodObjectV4 = z4.$ZodObject; +export type ZodTypeV4 = z4.$ZodType; + // eslint-disable-next-line @typescript-eslint/no-explicit-any export type InteropZodType = | z3.ZodType @@ -748,7 +750,9 @@ export function interopZodTransformInputSchema( if (recursive) { // Handle nested object schemas if (isZodObjectV4(outputSchema)) { - const outputShape: Mutable = outputSchema._zod.def.shape; + const outputShape: Mutable = { + ...outputSchema._zod.def.shape, + }; for (const [key, keySchema] of Object.entries( outputSchema._zod.def.shape )) { @@ -781,3 +785,27 @@ export function interopZodTransformInputSchema( throw new Error("Schema must be an instance of z3.ZodType or z4.$ZodType"); } + +/** + * Sanitizes a Zod schema by transforming it to its input schema and then making it strict. + * Supports both Zod v3 and v4 schemas. If `recursive` is true, applies strictness recursively to all nested object schemas and arrays of object schemas. + * + * @param schema - The Zod schema instance (v3 or v4) + * @param {boolean} [recursive=false] - Whether to recursively process nested objects/arrays. + * @returns The sanitized Zod schema. + */ +export function interopZodSanitizeSchema( + schema: InteropZodType, + recursive: boolean = false +): InteropZodType { + const inputSchema = interopZodTransformInputSchema(schema, recursive); + if (isZodObjectV4(inputSchema)) { + const strictSchema = interopZodObjectStrict( + inputSchema, + recursive + ) as ZodObjectV4; + return strictSchema; + } else { + return inputSchema; + } +} diff --git a/libs/langchain-openai/src/chat_models.ts b/libs/langchain-openai/src/chat_models.ts index aa0270d448ce..8b53b2cd0aee 100644 --- a/libs/langchain-openai/src/chat_models.ts +++ b/libs/langchain-openai/src/chat_models.ts @@ -71,6 +71,7 @@ import { getSchemaDescription, InteropZodType, isInteropZodSchema, + interopZodSanitizeSchema, } from "@langchain/core/utils/types"; import { toJsonSchema } from "@langchain/core/utils/json_schema"; import { @@ -1242,7 +1243,9 @@ export abstract class BaseChatOpenAI< const openaiJsonSchemaParams = { name: name ?? "extract", description: getSchemaDescription(schema), - schema, + schema: isInteropZodSchema(schema) + ? interopZodSanitizeSchema(schema, true) + : schema, strict: config?.strict, }; const asJsonSchema = toJsonSchema(openaiJsonSchemaParams.schema);