Skip to content
Merged
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
2 changes: 1 addition & 1 deletion langchain-core/src/utils/json_schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export { deepCompareStrict, Validator } from "@cfworker/json-schema";
*/
export function toJsonSchema(schema: InteropZodType | JSONSchema): JSONSchema {
if (isZodSchemaV4(schema)) {
const inputSchema = interopZodTransformInputSchema(schema);
const inputSchema = interopZodTransformInputSchema(schema, true);
if (isZodObjectV4(inputSchema)) {
const strictSchema = interopZodObjectStrict(
inputSchema,
Expand Down
35 changes: 33 additions & 2 deletions langchain-core/src/utils/tests/json_schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ describe("toJsonSchema", () => {
name: z4.string(),
age: z4.number(),
})
.describe("Object description")
.transform((data) => ({
...data,
upperName: data.name.toUpperCase(),
doubledAge: data.age * 2,
}));
}))
.describe("Object description");
const jsonSchema = toJsonSchema(schema);
expect(jsonSchema).toEqual({
$schema: "https://json-schema.org/draft/2020-12/schema",
Expand All @@ -30,5 +30,36 @@ describe("toJsonSchema", () => {
additionalProperties: false,
});
});
it("should allow v4 zod schemas with inner transforms", () => {
const userSchema = z4.object({
name: z4.string().transform((name) => Math.random() * name.length),
age: z4.number(),
});
const schema = z4
.object({
users: z4.array(userSchema).transform((users) => users.length),
count: z4.number().transform((count) => String(count * 2)),
})
.transform((data) => JSON.stringify(data));
const jsonSchema = toJsonSchema(schema);
expect(jsonSchema).toEqual({
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: {
users: {
type: "array",
items: {
type: "object",
additionalProperties: false,
properties: { name: { type: "string" }, age: { type: "number" } },
required: ["name", "age"],
},
},
count: { type: "number" },
},
required: ["users", "count"],
additionalProperties: false,
});
});
});
});
120 changes: 120 additions & 0 deletions langchain-core/src/utils/types/tests/zod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1405,6 +1405,126 @@ describe("Zod utility functions", () => {
const result = interopZodTransformInputSchema(inputSchema);
expect(result).toBe(inputSchema);
});

it("should handle recursive processing of nested object schemas", () => {
const nestedSchema = z4.object({
name: z4.string(),
age: z4.number(),
});
const inputSchema = z4.object({
user: nestedSchema,
metadata: z4.string(),
});
const transformSchema = inputSchema.transform((obj) => ({
...obj,
processed: true,
}));
const result = interopZodTransformInputSchema(transformSchema, true);

expect(result).toBeInstanceOf(z4.ZodObject);
const resultShape = getInteropZodObjectShape(result as any);
expect(Object.keys(resultShape)).toEqual(["user", "metadata"]);
expect(resultShape.user).toBeInstanceOf(z4.ZodObject);
expect(resultShape.metadata).toBeInstanceOf(z4.ZodString);
});

it("should handle recursive processing of arrays of object schemas", () => {
const userSchema = z4.object({
name: z4.string(),
age: z4.number(),
});
const inputSchema = z4.object({
users: z4.array(userSchema),
count: z4.number(),
});
const transformSchema = inputSchema.transform((obj) => ({
...obj,
processed: true,
}));
const result = interopZodTransformInputSchema(transformSchema, true);

expect(result).toBeInstanceOf(z4.ZodObject);
const resultShape = getInteropZodObjectShape(result as any);
expect(Object.keys(resultShape)).toEqual(["users", "count"]);
expect(resultShape.users).toBeInstanceOf(z4.ZodArray);
expect(resultShape.count).toBeInstanceOf(z4.ZodNumber);
});

it("should not apply recursive processing by default", () => {
const nestedSchema = z4.object({
name: z4.string(),
age: z4.number(),
});
const inputSchema = z4.object({
user: nestedSchema,
metadata: z4.string(),
});
const transformSchema = inputSchema.transform((obj) => ({
...obj,
processed: true,
}));
const result = interopZodTransformInputSchema(transformSchema);

// Should return the original input schema without recursive processing
expect(result).toBe(inputSchema);
});

it("should handle nested transforms in object properties", () => {
// Create a schema where inner properties are transformed
const userSchema = z4.object({
name: z4.string().transform((s) => s.toUpperCase()),
age: z4.number().transform((n) => n * 2),
});
const inputSchema = z4.object({
user: userSchema,
metadata: z4.string(),
});

// When recursive=true, we should get the input schema with the original property types
const result = interopZodTransformInputSchema(inputSchema, true);

expect(result).toBeInstanceOf(z4.ZodObject);
const resultShape = getInteropZodObjectShape(result as any);
expect(Object.keys(resultShape)).toEqual(["user", "metadata"]);

// The user property should be an object with untransformed schemas
expect(resultShape.user).toBeInstanceOf(z4.ZodObject);
const userShape = getInteropZodObjectShape(resultShape.user as any);
expect(Object.keys(userShape)).toEqual(["name", "age"]);
expect(userShape.name).toBeInstanceOf(z4.ZodString);
expect(userShape.age).toBeInstanceOf(z4.ZodNumber);

// The metadata should remain unchanged
expect(resultShape.metadata).toBeInstanceOf(z4.ZodString);
});

it("should handle transforms in array elements", () => {
// Create a schema where array elements are transformed
const userSchema = z4.object({
name: z4.string().transform((s) => s.toUpperCase()),
age: z4.number(),
});
const inputSchema = z4.object({
users: z4.array(userSchema),
count: z4.number(),
});

const result = interopZodTransformInputSchema(inputSchema, true);

expect(result).toBeInstanceOf(z4.ZodObject);
const resultShape = getInteropZodObjectShape(result as any);
expect(Object.keys(resultShape)).toEqual(["users", "count"]);

// The users property should be an array with untransformed element schema
expect(resultShape.users).toBeInstanceOf(z4.ZodArray);
const arrayElement = (resultShape.users as any)._zod.def.element;
expect(arrayElement).toBeInstanceOf(z4.ZodObject);

const elementShape = getInteropZodObjectShape(arrayElement as any);
expect(Object.keys(elementShape)).toEqual(["name", "age"]);
expect(elementShape.name).toBeInstanceOf(z4.ZodString);
expect(elementShape.age).toBeInstanceOf(z4.ZodNumber);
});
});

it("should throw error for non-schema values", () => {
Expand Down
53 changes: 46 additions & 7 deletions langchain-core/src/utils/types/zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
$ZodUnknown,
$ZodNever,
$ZodOptional,
_array,

Check warning on line 14 in langchain-core/src/utils/types/zod.ts

View workflow job for this annotation

GitHub Actions / Check linting

'_array' is defined but never used
} from "zod/v4/core";

export type ZodStringV3 = z3.ZodString;
Expand Down Expand Up @@ -715,29 +715,68 @@

/**
* Returns the input type of a Zod transform schema, for both v3 and v4.
* If the schema is not a transform, returns undefined.
* If the schema is not a transform, returns undefined. If `recursive` is true,
* recursively processes 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 input Zod schema of the transform, or undefined if not a transform
*/
export function interopZodTransformInputSchema(
schema: InteropZodType
): InteropZodType | undefined {
schema: InteropZodType,
recursive: boolean = false
): InteropZodType {
// Zod v3: ._def.schema is the input schema for ZodEffects (transform)
if (isZodSchemaV3(schema)) {
if (isZodTransformV3(schema)) {
return interopZodTransformInputSchema(schema._def.schema);
return interopZodTransformInputSchema(schema._def.schema, recursive);
}
// TODO: v3 schemas aren't recursively handled here
// (currently not necessary since zodToJsonSchema handles this)
return schema;
}

// Zod v4: _def.type is the input schema for ZodEffects (transform)
if (isZodSchemaV4(schema)) {
let outputSchema: InteropZodType = schema;
if (isZodTransformV4(schema)) {
const inner = interopZodTransformInputSchema(schema._zod.def.in);
return inner ?? schema;
outputSchema = interopZodTransformInputSchema(
schema._zod.def.in,
recursive
);
}
return schema;
if (recursive) {
// Handle nested object schemas
if (isZodObjectV4(outputSchema)) {
const outputShape: Mutable<z4.$ZodShape> = outputSchema._zod.def.shape;
for (const [key, keySchema] of Object.entries(
outputSchema._zod.def.shape
)) {
outputShape[key] = interopZodTransformInputSchema(
keySchema,
recursive
) as z4.$ZodType;
}
outputSchema = clone<ZodObjectV4>(outputSchema, {
...outputSchema._zod.def,
shape: outputShape,
});
}
// Handle nested array schemas
else if (isZodArrayV4(outputSchema)) {
const elementSchema = interopZodTransformInputSchema(
outputSchema._zod.def.element,
recursive
);
outputSchema = clone<z4.$ZodArray>(outputSchema, {
...outputSchema._zod.def,
element: elementSchema as z4.$ZodType,
});
}
}
const meta = globalRegistry.get(schema);
if (meta) globalRegistry.add(outputSchema as z4.$ZodType, meta);
return outputSchema;
}

throw new Error("Schema must be an instance of z3.ZodType or z4.$ZodType");
Expand Down
Loading