diff --git a/langchain-core/src/output_parsers/tests/structured.test.ts b/langchain-core/src/output_parsers/tests/structured.test.ts index d6f0f608a274..850788751dae 100644 --- a/langchain-core/src/output_parsers/tests/structured.test.ts +++ b/langchain-core/src/output_parsers/tests/structured.test.ts @@ -210,7 +210,7 @@ describe("StructuredOutputParser.fromZodSchema", () => { Here is the JSON Schema instance your output must adhere to. Include the enclosing markdown codeblock: \`\`\`json - {"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"answer":{"description":"answer to the user's question","type":"string"},"sources":{"description":"sources used to answer the question, should be websites.","type":"array","items":{"type":"string"}}},"required":["answer","sources"],"additionalProperties":false} + {"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"answer":{"description":"answer to the user's question","type":"string"},"sources":{"type":"array","items":{"type":"string"},"description":"sources used to answer the question, should be websites."}},"required":["answer","sources"],"additionalProperties":false} \`\`\` " `); @@ -343,7 +343,7 @@ describe("StructuredOutputParser.fromZodSchema", () => { Here is the JSON Schema instance your output must adhere to. Include the enclosing markdown codeblock: \`\`\`json - {"$schema":"https://json-schema.org/draft/2020-12/schema","description":"Only One object","type":"object","properties":{"url":{"description":"A link to the resource","type":"string"},"title":{"description":"A title for the resource","type":"string"},"year":{"description":"The year the resource was created","type":"number"},"createdAt":{"description":"The date and time the resource was created","type":"string"},"createdAtDate":{"description":"The date the resource was created","type":"string"},"authors":{"type":"array","items":{"type":"object","properties":{"name":{"description":"The name of the author","type":"string"},"email":{"description":"The email of the author","type":"string"},"type":{"type":"string","enum":["author","editor"]},"address":{"description":"The address of the author","type":"string"},"stateProvince":{"description":"The state or province of the author","type":"string","enum":["AL","AK","AZ"]}},"required":["name","email"],"additionalProperties":false}}},"required":["url","title","year","createdAt","authors"],"additionalProperties":false} + {"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"url":{"description":"A link to the resource","type":"string"},"title":{"description":"A title for the resource","type":"string"},"year":{"description":"The year the resource was created","type":"number"},"createdAt":{"description":"The date and time the resource was created","type":"string"},"createdAtDate":{"description":"The date the resource was created","type":"string"},"authors":{"type":"array","items":{"type":"object","properties":{"name":{"description":"The name of the author","type":"string"},"email":{"description":"The email of the author","type":"string"},"type":{"type":"string","enum":["author","editor"]},"address":{"description":"The address of the author","type":"string"},"stateProvince":{"description":"The state or province of the author","type":"string","enum":["AL","AK","AZ"]}},"required":["name","email"],"additionalProperties":false}}},"required":["url","title","year","createdAt","authors"],"additionalProperties":false,"description":"Only One object"} \`\`\` " `); diff --git a/langchain-core/src/utils/json_schema.ts b/langchain-core/src/utils/json_schema.ts index e87a85992cd0..6d48110e59ad 100644 --- a/langchain-core/src/utils/json_schema.ts +++ b/langchain-core/src/utils/json_schema.ts @@ -1,7 +1,15 @@ import { toJSONSchema } from "zod/v4/core"; import { type JsonSchema7Type, zodToJsonSchema } from "zod-to-json-schema"; import { dereference, type Schema } from "@cfworker/json-schema"; -import { isZodSchemaV3, isZodSchemaV4, InteropZodType } from "./types/zod.js"; +import { + isZodSchemaV3, + isZodSchemaV4, + InteropZodType, + interopZodObjectStrict, + isZodObjectV4, + ZodObjectV4, + interopZodTransformInputSchema, +} from "./types/zod.js"; export type JSONSchema = JsonSchema7Type; @@ -14,7 +22,16 @@ export { deepCompareStrict, Validator } from "@cfworker/json-schema"; */ export function toJsonSchema(schema: InteropZodType | JSONSchema): JSONSchema { if (isZodSchemaV4(schema)) { - return toJSONSchema(schema); + const inputSchema = interopZodTransformInputSchema(schema); + if (isZodObjectV4(inputSchema)) { + const strictSchema = interopZodObjectStrict( + inputSchema, + true + ) as ZodObjectV4; + return toJSONSchema(strictSchema); + } else { + return toJSONSchema(schema); + } } if (isZodSchemaV3(schema)) { return zodToJsonSchema(schema); diff --git a/langchain-core/src/utils/tests/json_schema.test.ts b/langchain-core/src/utils/tests/json_schema.test.ts new file mode 100644 index 000000000000..1cc524072152 --- /dev/null +++ b/langchain-core/src/utils/tests/json_schema.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect } from "@jest/globals"; +import { z as z4 } from "zod/v4"; +import { toJsonSchema } from "../json_schema.js"; + +describe("toJsonSchema", () => { + describe("with zod v4 schemas", () => { + // https://github.com/langchain-ai/langchainjs/issues/8367 + it("should allow transformed v4 zod schemas", () => { + const schema = z4 + .object({ + name: z4.string(), + age: z4.number(), + }) + .describe("Object description") + .transform((data) => ({ + ...data, + upperName: data.name.toUpperCase(), + doubledAge: data.age * 2, + })); + const jsonSchema = toJsonSchema(schema); + expect(jsonSchema).toEqual({ + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + description: "Object description", + properties: { + name: { type: "string" }, + age: { type: "number" }, + }, + required: ["name", "age"], + additionalProperties: false, + }); + }); + }); +}); diff --git a/langchain-core/src/utils/types/tests/zod.test.ts b/langchain-core/src/utils/types/tests/zod.test.ts index 7301200c18f7..92680bd69246 100644 --- a/langchain-core/src/utils/types/tests/zod.test.ts +++ b/langchain-core/src/utils/types/tests/zod.test.ts @@ -19,6 +19,9 @@ import { interopZodObjectPartial, interopZodObjectPassthrough, getInteropZodDefaultGetter, + interopZodObjectStrict, + ZodObjectV4, + interopZodTransformInputSchema, } from "../zod.js"; describe("Zod utility functions", () => { @@ -968,6 +971,127 @@ describe("Zod utility functions", () => { extra: "field", }); }); + + it("should handle recursive passthrough validation", () => { + const schema = z4.object({ + user: z4.strictObject({ + name: z4.string(), + age: z4.number(), + }), + }); + const passthrough = interopZodObjectPassthrough(schema, true); + expect( + interopParse(passthrough, { + user: { + name: "John", + age: 30, + extra: "field", + additional: 123, + }, + extra: "field", + }) + ).toEqual({ + user: { + name: "John", + age: 30, + extra: "field", + additional: 123, + }, + extra: "field", + }); + }); + + it("should not apply passthrough validation recursively by default", () => { + const schema = z4.object({ + user: z4.strictObject({ + name: z4.string(), + age: z4.number(), + }), + }); + const passthrough = interopZodObjectPassthrough(schema); + expect(() => + interopParse(passthrough, { + user: { + name: "John", + age: 30, + extra: "field", + }, + }) + ).toThrow(); + }); + + it("should add `additionalProperties: {}` when serialized to JSON schema", () => { + const schema = z4.object({ + name: z4.string(), + age: z4.number(), + }); + const passthrough = interopZodObjectPassthrough(schema) as ZodObjectV4; + const jsonSchema = z4.toJSONSchema(passthrough, { io: "input" }); + expect(jsonSchema.additionalProperties).toEqual({}); + }); + + it("should add `additionalProperties: {}` when serialized to JSON schema recursively", () => { + const schema = z4.object({ + user: z4.object({ + name: z4.string(), + age: z4.number(), + locations: z4.array( + z4.object({ + name: z4.string(), + }) + ), + }), + }); + const passthrough = interopZodObjectPassthrough( + schema, + true + ) as ZodObjectV4; + const jsonSchema = z4.toJSONSchema(passthrough, { io: "input" }); + expect(jsonSchema.additionalProperties).toEqual({}); + // @ts-expect-error - JSON schema types are not generic, but we still want to check the nested object + expect(jsonSchema.properties?.user?.additionalProperties).toEqual({}); + expect( + // @ts-expect-error - JSON schema types are not generic, but we still want to check the nested array + jsonSchema.properties?.user?.properties?.locations?.items + ?.additionalProperties + ).toEqual({}); + }); + + it("should handle arrays of objects with strict validation", () => { + const schema = z4.object({ + users: z4.array( + z4.object({ + name: z4.string(), + age: z4.number(), + }) + ), + }); + const strict = interopZodObjectStrict(schema, true); + expect(() => + interopParse(strict, { + users: [ + { name: "John", age: 30, extra: "field" }, + { name: "Jane", age: 25 }, + ], + }) + ).toThrow(); + }); + + it("should keep meta fields", () => { + const schema = z4 + .object({ + name: z4.string().describe("The name of the author"), + }) + .describe("The object"); + const passthrough = interopZodObjectPassthrough( + schema, + true + ) as ZodObjectV4; + expect(z4.globalRegistry.get(passthrough)).toBeDefined(); + expect(z4.globalRegistry.get(passthrough)?.description).toBe( + "The object" + ); + }); }); }); @@ -1058,4 +1182,238 @@ describe("Zod utility functions", () => { }); }); }); + + describe("interopZodObjectStrict", () => { + describe("v3 schemas", () => { + it("should make object schema strict", () => { + const schema = z3.object({ + name: z3.string(), + age: z3.number(), + }); + const strict = interopZodObjectStrict(schema); + expect(strict).toBeInstanceOf(z3.ZodObject); + const shape = getInteropZodObjectShape(strict); + expect(Object.keys(shape)).toEqual(["name", "age"]); + expect(shape.name).toBeInstanceOf(z3.ZodString); + expect(shape.age).toBeInstanceOf(z3.ZodNumber); + }); + + it("should reject extra properties", () => { + const schema = z3.object({ + name: z3.string(), + age: z3.number(), + }); + const strict = interopZodObjectStrict(schema); + expect(() => + interopParse(strict, { name: "John", age: 30, extra: "field" }) + ).toThrow(); + }); + }); + + describe("v4 schemas", () => { + it("should make object schema strict", () => { + const schema = z4.object({ + name: z4.string(), + age: z4.number(), + }); + const strict = interopZodObjectStrict(schema); + expect(strict).toBeInstanceOf(z4.ZodObject); + const shape = getInteropZodObjectShape(strict); + expect(Object.keys(shape)).toEqual(["name", "age"]); + expect(shape.name).toBeInstanceOf(z4.ZodString); + expect(shape.age).toBeInstanceOf(z4.ZodNumber); + }); + + it("should reject extra properties", () => { + const schema = z4.object({ + name: z4.string(), + age: z4.number(), + }); + const strict = interopZodObjectStrict(schema); + expect(() => + interopParse(strict, { name: "John", age: 30, extra: "field" }) + ).toThrow(); + }); + + it("should handle recursive strict validation", () => { + const schema = z4.object({ + user: z4.object({ + name: z4.string(), + age: z4.number(), + }), + }); + const strict = interopZodObjectStrict(schema, true); + expect(() => + interopParse(strict, { + user: { name: "John", age: 30, extra: "field" }, + }) + ).toThrow(); + }); + + it("should handle arrays of objects with strict validation", () => { + const schema = z4.object({ + users: z4.array( + z4.object({ + name: z4.string(), + age: z4.number(), + }) + ), + }); + const strict = interopZodObjectStrict(schema, true); + expect(() => + interopParse(strict, { + users: [ + { name: "John", age: 30, extra: "field" }, + { name: "Jane", age: 25 }, + ], + }) + ).toThrow(); + }); + + it("should not apply strict validation recursively by default", () => { + const schema = z4.object({ + user: z4.looseObject({ + name: z4.string(), + age: z4.number(), + }), + }); + const strict = interopZodObjectStrict(schema); + expect( + interopParse(strict, { + user: { name: "John", age: 30, extra: "field" }, + }) + ).toEqual({ + user: { name: "John", age: 30, extra: "field" }, + }); + }); + + it("should add `additionalProperties: false` when serialized to JSON schema", () => { + const schema = z4.object({ + name: z4.string(), + age: z4.number(), + }); + const strict = interopZodObjectStrict(schema) as ZodObjectV4; + const jsonSchema = z4.toJSONSchema(strict, { io: "input" }); + expect(jsonSchema.additionalProperties).toBe(false); + }); + + it("should add `additionalProperties: false` when serialized to JSON schema recursively", () => { + const schema = z4.object({ + user: z4.object({ + name: z4.string(), + age: z4.number(), + locations: z4.array( + z4.object({ + name: z4.string(), + }) + ), + }), + }); + const strict = interopZodObjectStrict(schema, true) as ZodObjectV4; + const jsonSchema = z4.toJSONSchema(strict, { io: "input" }); + expect(jsonSchema.additionalProperties).toBe(false); + // @ts-expect-error - JSON schema types are not generic, but we still want to check the nested object + expect(jsonSchema.properties?.user?.additionalProperties).toBe(false); + expect( + // @ts-expect-error - JSON schema types are not generic, but we still want to check the nested array + jsonSchema.properties?.user?.properties?.locations?.items + ?.additionalProperties + ).toBe(false); + }); + + it("should keep meta fields", () => { + const schema = z4 + .object({ + name: z4.string().describe("The name of the author"), + }) + .describe("The object"); + const strict = interopZodObjectStrict(schema, true) as ZodObjectV4; + expect(z4.globalRegistry.get(strict)).toBeDefined(); + expect(z4.globalRegistry.get(strict)?.description).toBe("The object"); + }); + }); + + it("should throw error for non-object schemas", () => { + // @ts-expect-error - Testing invalid input + expect(() => interopZodObjectStrict(z3.string())).toThrow(); + // @ts-expect-error - Testing invalid input + expect(() => interopZodObjectStrict(z4.string())).toThrow(); + // @ts-expect-error - Testing invalid input + expect(() => interopZodObjectStrict(z3.number())).toThrow(); + // @ts-expect-error - Testing invalid input + expect(() => interopZodObjectStrict(z4.number())).toThrow(); + // @ts-expect-error - Testing invalid input + expect(() => interopZodObjectStrict(z3.array(z3.string()))).toThrow(); + // @ts-expect-error - Testing invalid input + expect(() => interopZodObjectStrict(z4.array(z4.string()))).toThrow(); + }); + + it("should throw error for malformed schemas", () => { + // @ts-expect-error - Testing invalid input + expect(() => interopZodObjectStrict({})).toThrow(); + // @ts-expect-error - Testing invalid input + expect(() => interopZodObjectStrict({ _def: "fake" })).toThrow(); + // @ts-expect-error - Testing invalid input + expect(() => interopZodObjectStrict({ _zod: "fake" })).toThrow(); + }); + }); + + describe("interopZodTransformInputSchema", () => { + describe("v3 schemas", () => { + it("should return input schema for transform schema", () => { + const inputSchema = z3.string(); + const transformSchema = inputSchema.transform((s) => s.toUpperCase()); + const result = interopZodTransformInputSchema(transformSchema); + expect(result).toBe(inputSchema); + }); + + it("should return input schema for chained transforms", () => { + const inputSchema = z3.string(); + const transformSchema = inputSchema + .transform((s) => s.toUpperCase()) + .transform((s) => s.toLowerCase()); + const result = interopZodTransformInputSchema(transformSchema); + expect(result).toBe(inputSchema); + }); + + it("should return input schema for non-transform schema", () => { + const inputSchema = z3.string(); + const result = interopZodTransformInputSchema(inputSchema); + expect(result).toBe(inputSchema); + }); + }); + + describe("v4 schemas", () => { + it("should return input schema for transform schema", () => { + const inputSchema = z4.string(); + const transformSchema = inputSchema.transform((s) => s.toUpperCase()); + const result = interopZodTransformInputSchema(transformSchema); + expect(result).toBe(inputSchema); + }); + + it("should return input schema for chained transforms", () => { + const inputSchema = z4.string(); + const transformSchema = inputSchema + .transform((s) => s.toUpperCase()) + .transform((s) => s.toLowerCase()); + const result = interopZodTransformInputSchema(transformSchema); + expect(result).toBe(inputSchema); + }); + + it("should return input schema for non-transform schema", () => { + const inputSchema = z4.string(); + const result = interopZodTransformInputSchema(inputSchema); + expect(result).toBe(inputSchema); + }); + }); + + it("should throw error for non-schema values", () => { + expect(() => interopZodTransformInputSchema(null as any)).toThrow(); + expect(() => interopZodTransformInputSchema(undefined as any)).toThrow(); + expect(() => interopZodTransformInputSchema({} as any)).toThrow(); + expect(() => interopZodTransformInputSchema("string" as any)).toThrow(); + expect(() => interopZodTransformInputSchema(123 as any)).toThrow(); + expect(() => interopZodTransformInputSchema([] as any)).toThrow(); + }); + }); }); diff --git a/langchain-core/src/utils/types/zod.ts b/langchain-core/src/utils/types/zod.ts index 7a0dbba0f8d5..65605897cb68 100644 --- a/langchain-core/src/utils/types/zod.ts +++ b/langchain-core/src/utils/types/zod.ts @@ -7,8 +7,11 @@ import { util, clone, _unknown, + _never, $ZodUnknown, + $ZodNever, $ZodOptional, + _array, } from "zod/v4/core"; export type ZodStringV3 = z3.ZodString; @@ -62,6 +65,10 @@ export type InferInteropZodOutput = T extends z3.ZodType< ? Output : never; +export type Mutable = { + -readonly [P in keyof T]: T[P]; +}; + export function isZodSchemaV4( schema: unknown ): schema is z4.$ZodType { @@ -384,6 +391,62 @@ export function isSimpleStringZodSchema( return false; } +export function isZodObjectV3(obj: unknown): obj is ZodObjectV3 { + // Zod v3 object schemas have _def.typeName === "ZodObject" + if ( + typeof obj === "object" && + obj !== null && + "_def" in obj && + typeof obj._def === "object" && + obj._def !== null && + "typeName" in obj._def && + obj._def.typeName === "ZodObject" + ) { + return true; + } + return false; +} + +export function isZodObjectV4(obj: unknown): obj is z4.$ZodObject { + if (!isZodSchemaV4(obj)) return false; + // Zod v4 object schemas have _zod.def.type === "object" + if ( + typeof obj === "object" && + obj !== null && + "_zod" in obj && + typeof obj._zod === "object" && + obj._zod !== null && + "def" in obj._zod && + typeof obj._zod.def === "object" && + obj._zod.def !== null && + "type" in obj._zod.def && + obj._zod.def.type === "object" + ) { + return true; + } + return false; +} + +export function isZodArrayV4(obj: unknown): obj is z4.$ZodArray { + if (!isZodSchemaV4(obj)) return false; + // Zod v4 array schemas have _zod.def.type === "array" + if ( + typeof obj === "object" && + obj !== null && + "_zod" in obj && + typeof obj._zod === "object" && + obj._zod !== null && + "def" in obj._zod && + typeof obj._zod.def === "object" && + obj._zod.def !== null && + "type" in obj._zod.def && + obj._zod.def.type === "array" + ) { + return true; + } + return false; +} + /** * Determines if the provided value is an InteropZodObject (Zod v3 or v4 object schema). * @@ -391,37 +454,8 @@ export function isSimpleStringZodSchema( * @returns {boolean} True if the value is a Zod v3 or v4 object schema, false otherwise. */ export function isInteropZodObject(obj: unknown): obj is InteropZodObject { - if (isZodSchemaV3(obj)) { - // Zod v3 object schemas have _def.typeName === "ZodObject" - if ( - typeof obj === "object" && - obj !== null && - "_def" in obj && - typeof obj._def === "object" && - obj._def !== null && - "typeName" in obj._def && - obj._def.typeName === "ZodObject" - ) { - return true; - } - } - if (isZodSchemaV4(obj)) { - // Zod v4 object schemas have _zod.def.type === "object" - if ( - typeof obj === "object" && - obj !== null && - "_zod" in obj && - typeof obj._zod === "object" && - obj._zod !== null && - "def" in obj._zod && - typeof obj._zod.def === "object" && - obj._zod.def !== null && - "type" in obj._zod.def && - obj._zod.def.type === "object" - ) { - return true; - } - } + if (isZodObjectV3(obj)) return true; + if (isZodObjectV4(obj)) return true; return false; } @@ -496,21 +530,137 @@ export function interopZodObjectPartial( ); } -export function interopZodObjectPassthrough( - schema: T +/** + * Returns a strict version of a Zod object schema, disallowing unknown keys. + * Supports both Zod v3 and v4 object schemas. If `recursive` is true, applies strictness + * recursively to all nested object schemas and arrays of object schemas. + * + * @template T - The type of the Zod object schema. + * @param {T} schema - The Zod object schema instance (either v3 or v4). + * @param {boolean} [recursive=false] - Whether to apply strictness recursively to nested objects/arrays. + * @returns {InteropZodObject} The strict Zod object schema. + * @throws {Error} If the schema is not a Zod v3 or v4 object. + */ +export function interopZodObjectStrict( + schema: T, + recursive: boolean = false ): InteropZodObject { - if (isInteropZodObject(schema)) { - if (isZodSchemaV3(schema)) { - return schema.passthrough(); + if (isZodSchemaV3(schema)) { + // TODO: v3 schemas aren't recursively handled here + // (currently not necessary since zodToJsonSchema handles this) + return schema.strict(); + } + if (isZodObjectV4(schema)) { + const outputShape: Mutable = schema._zod.def.shape; + if (recursive) { + for (const [key, keySchema] of Object.entries(schema._zod.def.shape)) { + // If the shape key is a v4 object schema, we need to make it strict + if (isZodObjectV4(keySchema)) { + const outputSchema = interopZodObjectStrict(keySchema, recursive); + outputShape[key] = outputSchema as ZodObjectV4; + } + // If the shape key is a v4 array schema, we need to make the element + // schema strict if it's an object schema + else if (isZodArrayV4(keySchema)) { + let elementSchema = keySchema._zod.def.element; + if (isZodObjectV4(elementSchema)) { + elementSchema = interopZodObjectStrict( + elementSchema, + recursive + ) as ZodObjectV4; + } + outputShape[key] = clone(keySchema, { + ...keySchema._zod.def, + element: elementSchema, + }); + } + // Otherwise, just use the keySchema + else { + outputShape[key] = keySchema; + } + // Assign meta fields to the keySchema + const meta = globalRegistry.get(keySchema); + if (meta) globalRegistry.add(outputShape[key], meta); + } } - if (isZodSchemaV4(schema)) { - // Type reassign since ZodObjectV4 assumes that generics should be washed - const objectSchema: z4.$ZodObject = schema; - return clone(objectSchema, { - ...objectSchema._zod.def, - catchall: _unknown($ZodUnknown), - }); + const modifiedSchema = clone(schema, { + ...schema._zod.def, + shape: outputShape, + catchall: _never($ZodNever), + }); + const meta = globalRegistry.get(schema); + if (meta) globalRegistry.add(modifiedSchema, meta); + return modifiedSchema; + } + throw new Error( + "Schema must be an instance of z3.ZodObject or z4.$ZodObject" + ); +} + +/** + * Returns a passthrough version of a Zod object schema, allowing unknown keys. + * Supports both Zod v3 and v4 object schemas. If `recursive` is true, applies passthrough + * recursively to all nested object schemas and arrays of object schemas. + * + * @template T - The type of the Zod object schema. + * @param {T} schema - The Zod object schema instance (either v3 or v4). + * @param {boolean} [recursive=false] - Whether to apply passthrough recursively to nested objects/arrays. + * @returns {InteropZodObject} The passthrough Zod object schema. + * @throws {Error} If the schema is not a Zod v3 or v4 object. + */ +export function interopZodObjectPassthrough( + schema: T, + recursive: boolean = false +): InteropZodObject { + if (isZodObjectV3(schema)) { + // TODO: v3 schemas aren't recursively handled here + // (currently not necessary since zodToJsonSchema handles this) + return schema.passthrough(); + } + if (isZodObjectV4(schema)) { + const outputShape: Mutable = schema._zod.def.shape; + if (recursive) { + for (const [key, keySchema] of Object.entries(schema._zod.def.shape)) { + // If the shape key is a v4 object schema, we need to make it passthrough + if (isZodObjectV4(keySchema)) { + const outputSchema = interopZodObjectPassthrough( + keySchema, + recursive + ); + outputShape[key] = outputSchema as ZodObjectV4; + } + // If the shape key is a v4 array schema, we need to make the element + // schema passthrough if it's an object schema + else if (isZodArrayV4(keySchema)) { + let elementSchema = keySchema._zod.def.element; + if (isZodObjectV4(elementSchema)) { + elementSchema = interopZodObjectPassthrough( + elementSchema, + recursive + ) as ZodObjectV4; + } + outputShape[key] = clone(keySchema, { + ...keySchema._zod.def, + element: elementSchema, + }); + } + // Otherwise, just use the keySchema + else { + outputShape[key] = keySchema; + } + // Assign meta fields to the keySchema + const meta = globalRegistry.get(keySchema); + if (meta) globalRegistry.add(outputShape[key], meta); + } } + const modifiedSchema = clone(schema, { + ...schema._zod.def, + shape: outputShape, + catchall: _unknown($ZodUnknown), + }); + const meta = globalRegistry.get(schema); + if (meta) globalRegistry.add(modifiedSchema, meta); + return modifiedSchema; } throw new Error( "Schema must be an instance of z3.ZodObject or z4.$ZodObject" @@ -548,3 +698,47 @@ export function getInteropZodDefaultGetter( } return undefined; } + +function isZodTransformV3( + schema: InteropZodType +): schema is z3.ZodEffects { + return ( + isZodSchemaV3(schema) && + "typeName" in schema._def && + schema._def.typeName === "ZodEffects" + ); +} + +function isZodTransformV4(schema: InteropZodType): schema is z4.$ZodPipe { + return isZodSchemaV4(schema) && schema._zod.def.type === "pipe"; +} + +/** + * Returns the input type of a Zod transform schema, for both v3 and v4. + * If the schema is not a transform, returns undefined. + * + * @param schema - The Zod schema instance (v3 or v4) + * @returns The input Zod schema of the transform, or undefined if not a transform + */ +export function interopZodTransformInputSchema( + schema: InteropZodType +): InteropZodType | undefined { + // Zod v3: ._def.schema is the input schema for ZodEffects (transform) + if (isZodSchemaV3(schema)) { + if (isZodTransformV3(schema)) { + return interopZodTransformInputSchema(schema._def.schema); + } + return schema; + } + + // Zod v4: _def.type is the input schema for ZodEffects (transform) + if (isZodSchemaV4(schema)) { + if (isZodTransformV4(schema)) { + const inner = interopZodTransformInputSchema(schema._zod.def.in); + return inner ?? schema; + } + return schema; + } + + throw new Error("Schema must be an instance of z3.ZodType or z4.$ZodType"); +}