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
14 changes: 11 additions & 3 deletions libs/langchain-core/src/utils/json_schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,30 @@ import {

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

export type ToJSONSchemaParams = NonNullable<
Parameters<typeof toJSONSchema>[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)) {
const strictSchema = interopZodObjectStrict(
inputSchema,
true
) as ZodObjectV4;
return toJSONSchema(strictSchema);
return toJSONSchema(strictSchema, _params);
} else {
return toJSONSchema(schema);
return toJSONSchema(schema, _params);
}
}
if (isZodSchemaV3(schema)) {
Expand Down
41 changes: 41 additions & 0 deletions libs/langchain-core/src/utils/types/tests/zod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
2 changes: 1 addition & 1 deletion libs/langchain-core/src/utils/types/zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -814,7 +814,7 @@ 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> = {};
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
5 changes: 3 additions & 2 deletions libs/providers/langchain-openai/src/utils/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -104,7 +105,7 @@ export function interopZodResponseFormat(
...props,
name,
strict: true,
schema: toJSONSchemaV4(zodSchema, {
schema: toJsonSchema(zodSchema, {
Copy link
Author

Choose a reason for hiding this comment

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

Call the version of toJsonSchema that also strips off transforms, since we can no longer assume that that has already been done for us by this point.

cycles: "ref", // equivalent to nameStrategy: 'duplicate-ref'
reused: "ref", // equivalent to $refStrategy: 'extract-to-root'
override(ctx) {
Expand Down