Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 4 additions & 14 deletions langchain-core/src/utils/json_schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@ import {
isZodSchemaV3,
isZodSchemaV4,
InteropZodType,
interopZodObjectStrict,
isZodObjectV4,
ZodObjectV4,
interopZodTransformInputSchema,
ZodTypeV4,
interopZodSanitizeSchema,
} from "./types/zod.js";

export type JSONSchema = JsonSchema7Type;
Expand All @@ -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);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I correct to assume that toJSONSchema(schema) was a typo and that it should have been toJSONSchema(inputSchema)?

If not, please let me know, as this PR effectively makes that change.

}
const inputSchema = interopZodSanitizeSchema(schema, true);
return toJSONSchema(inputSchema as ZodTypeV4);
}
if (isZodSchemaV3(schema)) {
return zodToJsonSchema(schema);
Expand Down
42 changes: 42 additions & 0 deletions langchain-core/src/utils/types/tests/zod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
30 changes: 29 additions & 1 deletion langchain-core/src/utils/types/zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export type ZodObjectV3 = z3.ZodObject<any, any, any, any>;

export type ZodObjectV4 = z4.$ZodObject;

export type ZodTypeV4 = z4.$ZodType;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type InteropZodType<Output = any, Input = Output> =
| z3.ZodType<Output, z3.ZodTypeDef, Input>
Expand Down Expand Up @@ -748,7 +750,9 @@ export function interopZodTransformInputSchema(
if (recursive) {
// Handle nested object schemas
if (isZodObjectV4(outputSchema)) {
const outputShape: Mutable<z4.$ZodShape> = outputSchema._zod.def.shape;
const outputShape: Mutable<z4.$ZodShape> = {
...outputSchema._zod.def.shape,
};
Comment on lines +753 to +755
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Primary fix here.

for (const [key, keySchema] of Object.entries(
outputSchema._zod.def.shape
)) {
Expand Down Expand Up @@ -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(
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted so that it can be called independently here.

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;
}
}
5 changes: 4 additions & 1 deletion libs/langchain-openai/src/chat_models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import {
getSchemaDescription,
InteropZodType,
isInteropZodSchema,
interopZodSanitizeSchema,
} from "@langchain/core/utils/types";
import { toJsonSchema } from "@langchain/core/utils/json_schema";
import {
Expand Down Expand Up @@ -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);
Expand Down