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
21 changes: 20 additions & 1 deletion packages/zod/src/v4/classic/tests/lazy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<A> = z.lazy(() =>
z.object({
val: z.number(),
b: Blazy,
})
);

const Blazy: z.ZodType<B> = 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", () => {
Expand Down
38 changes: 38 additions & 0 deletions packages/zod/src/v4/core/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4132,6 +4132,8 @@ export const $ZodPromise: core.$constructor<$ZodPromise> = /*@__PURE__*/ core.$c
//////////////////////////////////////////
//////////////////////////////////////////

const LAZY_PARSE_MEMO = Symbol("zod.lazy.memo");

export interface $ZodLazyDef<T extends SomeType = $ZodType> extends $ZodTypeDef {
type: "lazy";
getter: () => T;
Expand Down Expand Up @@ -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<object, Map<$ZodLazy, util.MaybeAsync<ParsePayload>>>
| undefined;
if (!memoRoot) {
memoRoot = new WeakMap<object, Map<$ZodLazy, util.MaybeAsync<ParsePayload>>>();
(ctx as any)[LAZY_PARSE_MEMO] = memoRoot;
}
let schemaCache = memoRoot.get(value) as Map<$ZodLazy, util.MaybeAsync<ParsePayload>> | 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);
};
});
Expand Down