From 8f8a252a2ce8868ac29ce8d5f44b602b2a40d700 Mon Sep 17 00:00:00 2001 From: Nathan Rihet <50133907+NathanKneT@users.noreply.github.com> Date: Wed, 13 Aug 2025 17:43:34 +0900 Subject: [PATCH] fix(core): prevent infinite recursion for recursive tuples (#5089) - Add optional chaining to $ZodLazy and $ZodReadonly property access - Prevents "Cannot read properties of undefined (reading '_zod')" errors - Add regression tests for recursive tuple patterns - Maintains backward compatibility and performance Fixes colinhacks/zod#5089 --- packages/zod/src/v4/core/schemas.ts | 12 ++--- .../v4/core/tests/recursive-tuples.test.ts | 45 +++++++++++++++++++ 2 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 packages/zod/src/v4/core/tests/recursive-tuples.test.ts diff --git a/packages/zod/src/v4/core/schemas.ts b/packages/zod/src/v4/core/schemas.ts index 93e2949dc2..33bc8879b9 100644 --- a/packages/zod/src/v4/core/schemas.ts +++ b/packages/zod/src/v4/core/schemas.ts @@ -3545,8 +3545,8 @@ export const $ZodReadonly: core.$constructor<$ZodReadonly> = /*@__PURE__*/ core. $ZodType.init(inst, def); util.defineLazy(inst._zod, "propValues", () => def.innerType._zod.propValues); util.defineLazy(inst._zod, "values", () => def.innerType._zod.values); - util.defineLazy(inst._zod, "optin", () => def.innerType._zod.optin); - util.defineLazy(inst._zod, "optout", () => def.innerType._zod.optout); + util.defineLazy(inst._zod, "optin", () => def.innerType?._zod?.optin); + util.defineLazy(inst._zod, "optout", () => def.innerType?._zod?.optout); inst._zod.parse = (payload, ctx) => { const result = def.innerType._zod.run(payload, ctx); @@ -3767,10 +3767,10 @@ export const $ZodLazy: core.$constructor<$ZodLazy> = /*@__PURE__*/ core.$constru // return () => _innerType; // }); util.defineLazy(inst._zod, "innerType", () => def.getter() as $ZodType); - util.defineLazy(inst._zod, "pattern", () => inst._zod.innerType._zod.pattern); - util.defineLazy(inst._zod, "propValues", () => inst._zod.innerType._zod.propValues); - util.defineLazy(inst._zod, "optin", () => inst._zod.innerType._zod.optin ?? undefined); - util.defineLazy(inst._zod, "optout", () => inst._zod.innerType._zod.optout ?? undefined); + util.defineLazy(inst._zod, "pattern", () => inst._zod.innerType?._zod?.pattern); + util.defineLazy(inst._zod, "propValues", () => inst._zod.innerType?._zod?.propValues); + util.defineLazy(inst._zod, "optin", () => inst._zod.innerType?._zod?.optin ?? undefined); + util.defineLazy(inst._zod, "optout", () => inst._zod.innerType?._zod?.optout ?? undefined); inst._zod.parse = (payload, ctx) => { const inner = inst._zod.innerType; return inner._zod.run(payload, ctx); diff --git a/packages/zod/src/v4/core/tests/recursive-tuples.test.ts b/packages/zod/src/v4/core/tests/recursive-tuples.test.ts new file mode 100644 index 0000000000..c8d0e631be --- /dev/null +++ b/packages/zod/src/v4/core/tests/recursive-tuples.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from "vitest"; +import * as z from "zod/v4"; + +describe("Recursive Tuples Regression #5089", () => { + it("creates recursive tuple without crash", () => { + expect(() => { + const y = z.lazy((): any => z.tuple([y, y]).or(z.string())); + }).not.toThrow(); + }); + + it("parses recursive tuple data correctly", () => { + const y = z.lazy((): any => z.tuple([y, y]).or(z.string())); + + // Base case + expect(y.parse("hello")).toBe("hello"); + + // Recursive cases + expect(() => y.parse(["a", "b"])).not.toThrow(); + expect(() => y.parse(["a", ["b", "c"]])).not.toThrow(); + }); + + it("matches #5089 expected behavior", () => { + // Exact code from the issue + expect(() => { + const y = z.lazy((): any => z.tuple([y, y]).or(z.string())); + y.parse(["a", ["b", "c"]]); + }).not.toThrow(); + }); + + it("handles workaround pattern", () => { + // Alternative pattern from issue discussion + expect(() => { + const y = z.lazy((): any => z.string().or(z.lazy(() => z.tuple([y, y])))); + y.parse(["a", ["b", "c"]]); + }).not.toThrow(); + }); + + it("recursive arrays still work (comparison)", () => { + const y = z.lazy((): any => z.array(y).or(z.string())); + + expect(y.parse("hello")).toBe("hello"); + expect(y.parse(["hello", "world"])).toEqual(["hello", "world"]); + expect(y.parse(["a", ["b", "c"]])).toEqual(["a", ["b", "c"]]); + }); +});