diff --git a/libs/langchain-core/src/utils/json_schema.ts b/libs/langchain-core/src/utils/json_schema.ts index 4e4a0f5f4dd6..c27ae2814760 100644 --- a/libs/langchain-core/src/utils/json_schema.ts +++ b/libs/langchain-core/src/utils/json_schema.ts @@ -16,12 +16,20 @@ import { export { deepCompareStrict, Validator } from "@cfworker/json-schema"; +export type ToJSONSchemaParams = NonNullable< + Parameters[1] +>; + /** * Converts a Zod schema or JSON schema to a JSON schema. * @param schema - The schema to convert. + * @param _params - The parameters to pass to the toJSONSchema function. * @returns The converted schema. */ -export function toJsonSchema(schema: InteropZodType | JSONSchema): JSONSchema { +export function toJsonSchema( + schema: InteropZodType | JSONSchema, + _params?: ToJSONSchemaParams +): JSONSchema { if (isZodSchemaV4(schema)) { const inputSchema = interopZodTransformInputSchema(schema, true); if (isZodObjectV4(inputSchema)) { @@ -29,9 +37,9 @@ export function toJsonSchema(schema: InteropZodType | JSONSchema): JSONSchema { inputSchema, true ) as ZodObjectV4; - return toJSONSchema(strictSchema); + return toJSONSchema(strictSchema, _params); } else { - return toJSONSchema(schema); + return toJSONSchema(schema, _params); } } if (isZodSchemaV3(schema)) { diff --git a/libs/langchain-core/src/utils/types/tests/zod.test.ts b/libs/langchain-core/src/utils/types/tests/zod.test.ts index 7d7cabad9b25..512c49a22edd 100644 --- a/libs/langchain-core/src/utils/types/tests/zod.test.ts +++ b/libs/langchain-core/src/utils/types/tests/zod.test.ts @@ -1525,6 +1525,47 @@ describe("Zod utility functions", () => { expect(elementShape.name).toBeInstanceOf(z4.ZodString); expect(elementShape.age).toBeInstanceOf(z4.ZodNumber); }); + + it("should not mutate the original schema when removing transforms", () => { + const inputSchema = z4.object({ + name: z4.string().transform((s) => s.toUpperCase()), + email: z4 + .string() + .email() + .transform((s) => s.toLowerCase()), + age: z4.number(), + metadata: z4.object({ + key: z4.string(), + value: z4.string().transform((s) => s.trim()), + }), + }); + + // Capture the original schema structure before processing + const originalSchemaJson = JSON.stringify(inputSchema); + + // Process the schema + const result = interopZodTransformInputSchema(inputSchema, true); + + // Verify the original schema is unchanged + const schemaJsonAfter = JSON.stringify(inputSchema); + expect(schemaJsonAfter).toBe(originalSchemaJson); + + // Verify that the result is different from the original + const resultJson = JSON.stringify(result); + expect(resultJson).not.toBe(originalSchemaJson); + + // Verify the result actually has transforms removed + expect(result).toBeInstanceOf(z4.ZodObject); + const resultShape = getInteropZodObjectShape(result as any); + expect(resultShape.name).toBeInstanceOf(z4.ZodString); + expect(resultShape.email).toBeInstanceOf(z4.ZodString); + expect(resultShape.age).toBeInstanceOf(z4.ZodNumber); + + const metadataShape = getInteropZodObjectShape( + resultShape.metadata as any + ); + expect(metadataShape.value).toBeInstanceOf(z4.ZodString); + }); }); it("should throw error for non-schema values", () => { diff --git a/libs/langchain-core/src/utils/types/zod.ts b/libs/langchain-core/src/utils/types/zod.ts index b2e902ac2c8b..01df3602a19b 100644 --- a/libs/langchain-core/src/utils/types/zod.ts +++ b/libs/langchain-core/src/utils/types/zod.ts @@ -814,7 +814,7 @@ export function interopZodTransformInputSchema( if (recursive) { // Handle nested object schemas if (isZodObjectV4(outputSchema)) { - const outputShape: Mutable = outputSchema._zod.def.shape; + const outputShape: Mutable = {}; for (const [key, keySchema] of Object.entries( outputSchema._zod.def.shape )) { diff --git a/libs/providers/langchain-openai/src/utils/output.ts b/libs/providers/langchain-openai/src/utils/output.ts index a4a9868c9f4e..67c045850a4c 100644 --- a/libs/providers/langchain-openai/src/utils/output.ts +++ b/libs/providers/langchain-openai/src/utils/output.ts @@ -4,10 +4,11 @@ import { isZodSchemaV3, isZodSchemaV4, } from "@langchain/core/utils/types"; -import { toJSONSchema as toJSONSchemaV4, parse as parseV4 } from "zod/v4/core"; +import { parse as parseV4 } from "zod/v4/core"; import { ResponseFormatJSONSchema } from "openai/resources"; import { zodResponseFormat } from "openai/helpers/zod"; import { ContentBlock, UsageMetadata } from "@langchain/core/messages"; +import { toJsonSchema } from "@langchain/core/utils/json_schema"; const SUPPORTED_METHODS = [ "jsonSchema", @@ -104,7 +105,7 @@ export function interopZodResponseFormat( ...props, name, strict: true, - schema: toJSONSchemaV4(zodSchema, { + schema: toJsonSchema(zodSchema, { cycles: "ref", // equivalent to nameStrategy: 'duplicate-ref' reused: "ref", // equivalent to $refStrategy: 'extract-to-root' override(ctx) {