diff --git a/packages/zod/src/v4/classic/tests/lazy.test.ts b/packages/zod/src/v4/classic/tests/lazy.test.ts index fb0234803..6ff641db5 100644 --- a/packages/zod/src/v4/classic/tests/lazy.test.ts +++ b/packages/zod/src/v4/classic/tests/lazy.test.ts @@ -172,12 +172,31 @@ test("mutual recursion with lazy", () => { expect(() => Alazy.parse({ val: "asdf" })).toThrow(); }); -// TODO test("mutual recursion with cyclical data", () => { + const Alazy: z.ZodType = z.lazy(() => + z.object({ + val: z.number(), + b: Blazy, + }) + ); + + const Blazy: z.ZodType = z.lazy(() => + z.object({ + val: z.number(), + a: Alazy.optional(), + }) + ); + const a: any = { val: 1 }; const b: any = { val: 2 }; a.b = b; b.a = a; + + const parsedA = Alazy.parse(a, { jitless: true }); + expect(parsedA.b.a).toBe(parsedA); + + const parsedB = Blazy.parse(b, { jitless: true }); + expect(parsedB.a?.b).toBe(parsedB); }); test("complicated self-recursion", () => { diff --git a/packages/zod/src/v4/core/schemas.ts b/packages/zod/src/v4/core/schemas.ts index b8c491e49..db88c4b21 100644 --- a/packages/zod/src/v4/core/schemas.ts +++ b/packages/zod/src/v4/core/schemas.ts @@ -4132,6 +4132,8 @@ export const $ZodPromise: core.$constructor<$ZodPromise> = /*@__PURE__*/ core.$c ////////////////////////////////////////// ////////////////////////////////////////// +const LAZY_PARSE_MEMO = Symbol("zod.lazy.memo"); + export interface $ZodLazyDef extends $ZodTypeDef { type: "lazy"; getter: () => T; @@ -4170,6 +4172,42 @@ export const $ZodLazy: core.$constructor<$ZodLazy> = /*@__PURE__*/ core.$constru util.defineLazy(inst._zod, "optout", () => inst._zod.innerType._zod.optout ?? undefined); inst._zod.parse = (payload, ctx) => { const inner = inst._zod.innerType; + const value = payload.value; + + if (typeof value === "object" && value !== null) { + let memoRoot = (ctx as any)[LAZY_PARSE_MEMO] as + | WeakMap>> + | undefined; + if (!memoRoot) { + memoRoot = new WeakMap>>(); + (ctx as any)[LAZY_PARSE_MEMO] = memoRoot; + } + let schemaCache = memoRoot.get(value) as Map<$ZodLazy, util.MaybeAsync> | undefined; + + if (!schemaCache) { + schemaCache = new Map(); + memoRoot.set(value, schemaCache); + } else { + const cached = schemaCache.get(inst); + if (cached) { + return cached; + } + } + + schemaCache.set(inst, payload); + const result = inner._zod.run(payload, ctx); + if (result instanceof Promise) { + const memoized = result.then((resolved) => { + schemaCache!.set(inst, resolved); + return resolved; + }); + schemaCache.set(inst, memoized); + return memoized; + } + schemaCache.set(inst, result); + return result; + } + return inner._zod.run(payload, ctx); }; });