Skip to content

Commit 2313a71

Browse files
committed
fix(langchain): Fix toJsonSchema mutating underlying zod schema
1 parent 5d8b75f commit 2313a71

File tree

3 files changed

+51
-2
lines changed

3 files changed

+51
-2
lines changed

libs/langchain-core/src/utils/json_schema.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,20 @@ import {
1616

1717
export { deepCompareStrict, Validator } from "@cfworker/json-schema";
1818

19+
export type ToJSONSchemaParams = NonNullable<
20+
Parameters<typeof toJSONSchema>[1]
21+
>;
22+
1923
/**
2024
* Converts a Zod schema or JSON schema to a JSON schema.
2125
* @param schema - The schema to convert.
26+
* @param _params - The parameters to pass to the toJSONSchema function.
2227
* @returns The converted schema.
2328
*/
24-
export function toJsonSchema(schema: InteropZodType | JSONSchema): JSONSchema {
29+
export function toJsonSchema(
30+
schema: InteropZodType | JSONSchema,
31+
_params?: ToJSONSchemaParams
32+
): JSONSchema {
2533
if (isZodSchemaV4(schema)) {
2634
const inputSchema = interopZodTransformInputSchema(schema, true);
2735
if (isZodObjectV4(inputSchema)) {

libs/langchain-core/src/utils/types/tests/zod.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1525,6 +1525,47 @@ describe("Zod utility functions", () => {
15251525
expect(elementShape.name).toBeInstanceOf(z4.ZodString);
15261526
expect(elementShape.age).toBeInstanceOf(z4.ZodNumber);
15271527
});
1528+
1529+
it("should not mutate the original schema when removing transforms", () => {
1530+
const inputSchema = z4.object({
1531+
name: z4.string().transform((s) => s.toUpperCase()),
1532+
email: z4
1533+
.string()
1534+
.email()
1535+
.transform((s) => s.toLowerCase()),
1536+
age: z4.number(),
1537+
metadata: z4.object({
1538+
key: z4.string(),
1539+
value: z4.string().transform((s) => s.trim()),
1540+
}),
1541+
});
1542+
1543+
// Capture the original schema structure before processing
1544+
const originalSchemaJson = JSON.stringify(inputSchema);
1545+
1546+
// Process the schema
1547+
const result = interopZodTransformInputSchema(inputSchema, true);
1548+
1549+
// Verify the original schema is unchanged
1550+
const schemaJsonAfter = JSON.stringify(inputSchema);
1551+
expect(schemaJsonAfter).toBe(originalSchemaJson);
1552+
1553+
// Verify that the result is different from the original
1554+
const resultJson = JSON.stringify(result);
1555+
expect(resultJson).not.toBe(originalSchemaJson);
1556+
1557+
// Verify the result actually has transforms removed
1558+
expect(result).toBeInstanceOf(z4.ZodObject);
1559+
const resultShape = getInteropZodObjectShape(result as any);
1560+
expect(resultShape.name).toBeInstanceOf(z4.ZodString);
1561+
expect(resultShape.email).toBeInstanceOf(z4.ZodString);
1562+
expect(resultShape.age).toBeInstanceOf(z4.ZodNumber);
1563+
1564+
const metadataShape = getInteropZodObjectShape(
1565+
resultShape.metadata as any
1566+
);
1567+
expect(metadataShape.value).toBeInstanceOf(z4.ZodString);
1568+
});
15281569
});
15291570

15301571
it("should throw error for non-schema values", () => {

libs/langchain-core/src/utils/types/zod.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -814,7 +814,7 @@ export function interopZodTransformInputSchema(
814814
if (recursive) {
815815
// Handle nested object schemas
816816
if (isZodObjectV4(outputSchema)) {
817-
const outputShape: Mutable<z4.$ZodShape> = outputSchema._zod.def.shape;
817+
const outputShape: Mutable<z4.$ZodShape> = {};
818818
for (const [key, keySchema] of Object.entries(
819819
outputSchema._zod.def.shape
820820
)) {

0 commit comments

Comments
 (0)