Skip to content

Conversation

@mohankumarelec
Copy link

Fix TypeError when creating self-referencing lazy schemas

Problem

When creating a lazy schema that self-references itself, Zod throws a TypeError:

const schema = z.lazy(() => z.tuple([schema]));

This results in the following error:

packages/zod/src/v4/core/schemas.ts:4169
  util.defineLazy(inst._zod, "optin", () => inst._zod.innerType._zod.optin ?? undefined);
                                                                ^

TypeError: Cannot read properties of undefined (reading '_zod')

Solution

This PR fixes the issue by adding proper optional chaining to handle cases where innerType might be undefined during the lazy evaluation of self-referencing schemas.

@mohankumarelec
Copy link
Author

@colinhacks, Can you please take a look into this PR and let me know if anything further required. Also please feel free to edit the PR if required. Thanks

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 22, 2025

Walkthrough

The packages/zod/src/v4/core/schemas.ts file has been updated to modify how the ZodLazy wrapper handles lazy-initialization. The optin and optout getters now use optional chaining (innerType?._zod.optin and innerType?._zod.optout) instead of direct property access. This guards against potential runtime errors when innerType hasn't been initialized yet. Other lazy properties and the core evaluation path remain unchanged.

Pre-merge checks

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The PR title "fix: Fix optional chaining for innerType in $ZodLazy" directly and specifically describes the main change in the changeset. It clearly references the addition of optional chaining to the innerType property in the $ZodLazy wrapper, which is exactly what the code changes implement. The title is concise, readable, and specific enough that a teammate scanning history would understand the primary change without confusion.
Description Check ✅ Passed The PR description is clearly related to the changeset and provides meaningful context about the problem being fixed. It explains the specific issue with self-referencing lazy schemas, includes a concrete code example that reproduces the error, shows the exact TypeError being thrown, and describes how the fix uses optional chaining to handle undefined innerType. The description is neither vague nor off-topic—it directly connects the problem, error, and solution to the actual code changes.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5e6c0fd and 1a949df.

📒 Files selected for processing (1)
  • packages/zod/src/v4/core/schemas.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (11)
**/*.{js,jsx,ts,tsx,mjs,cjs,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Enforce line width of 120 characters via Biome formatting

Files:

  • packages/zod/src/v4/core/schemas.ts
**/*.{js,jsx,ts,tsx,mjs,cjs}

📄 CodeRabbit inference engine (CLAUDE.md)

Use ES5-style trailing commas in JavaScript/TypeScript code

Files:

  • packages/zod/src/v4/core/schemas.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Allow the any type in TypeScript (noExplicitAny off)
Allow non-null assertions in TypeScript (noNonNullAssertion off)
Write TypeScript to pass strict mode with exactOptionalPropertyTypes enabled
Use NodeNext module resolution semantics for imports in TypeScript
Target ES2020 language features in TypeScript source

Files:

  • packages/zod/src/v4/core/schemas.ts
**/*.{ts,tsx,js,jsx,mjs,cjs}

📄 CodeRabbit inference engine (CLAUDE.md)

Allow parameter reassignment for performance-sensitive code (noParameterAssign off)

Files:

  • packages/zod/src/v4/core/schemas.ts
**/*.{js,mjs,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/development-setup.mdc)

**/*.{js,mjs,ts,tsx}: Use .js extensions in import specifiers (e.g., import { z } from "./index.js")
Don’t use require(); use ESM import statements

Files:

  • packages/zod/src/v4/core/schemas.ts
**/*.{js,mjs,cjs,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/guidelines.mdc)

Do not leave log statements (e.g., console.log, debugger) in tests or production code

Files:

  • packages/zod/src/v4/core/schemas.ts
packages/zod/src/v4/core/{schemas.ts,core.ts}

📄 CodeRabbit inference engine (.cursor/rules/zod-internals.mdc)

Use the custom constructor system via core.$constructor() and initialize instances with $ZodType.init() when creating schemas

Files:

  • packages/zod/src/v4/core/schemas.ts
packages/zod/src/v4/core/schemas.ts

📄 CodeRabbit inference engine (.cursor/rules/zod-internals.mdc)

packages/zod/src/v4/core/schemas.ts: Wrapper schemas (e.g., $ZodOptional, $ZodNullable, $ZodReadonly) must pass through internal properties from their inner type using util.defineLazy for propValues, values, optin, and optout
Define computed internal properties using util.defineLazy() to avoid circular dependencies
Implement schema parse functions following the standard structure: type check, push invalid_type issue on mismatch, optionally coerce/transform, and return payload
For $ZodDiscriminatedUnion, compute and merge propValues lazily from options; ensure each option provides the discriminator key and that values are unique
Ensure readonly wrapper types (e.g., $ZodReadonly) pass through values for discriminator support in unions

Files:

  • packages/zod/src/v4/core/schemas.ts
packages/zod/src/v4/core/{schemas.ts,checks.ts}

📄 CodeRabbit inference engine (.cursor/rules/zod-internals.mdc)

When adding issues, push well-formed payload.issues entries including code, expected (when applicable), input, inst, and optional path/message/continue

Files:

  • packages/zod/src/v4/core/schemas.ts
packages/**/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/zod-project-guide.mdc)

Write source code in TypeScript (TypeScript-first codebase)

Files:

  • packages/zod/src/v4/core/schemas.ts
packages/zod/**

📄 CodeRabbit inference engine (.cursor/rules/zod-project-guide.mdc)

Make core Zod library changes in the main package at packages/zod/

Files:

  • packages/zod/src/v4/core/schemas.ts
🧠 Learnings (4)
📓 Common learnings
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.477Z
Learning: Applies to packages/zod/src/v4/core/schemas.ts : Wrapper schemas (e.g., $ZodOptional, $ZodNullable, $ZodReadonly) must pass through internal properties from their inner type using util.defineLazy for propValues, values, optin, and optout
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.477Z
Learning: Applies to packages/zod/src/v4/core/schemas.ts : Define computed internal properties using util.defineLazy() to avoid circular dependencies
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.477Z
Learning: Applies to packages/zod/src/v4/core/{schemas.ts,core.ts} : Use the custom constructor system via core.$constructor() and initialize instances with $ZodType.init() when creating schemas
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.477Z
Learning: Applies to packages/zod/src/v4/core/schemas.ts : Ensure readonly wrapper types (e.g., $ZodReadonly) pass through values for discriminator support in unions
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.477Z
Learning: Applies to packages/zod/src/v4/core/schemas.ts : For $ZodDiscriminatedUnion, compute and merge propValues lazily from options; ensure each option provides the discriminator key and that values are unique
📚 Learning: 2025-10-21T17:27:32.477Z
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.477Z
Learning: Applies to packages/zod/src/v4/core/schemas.ts : Wrapper schemas (e.g., $ZodOptional, $ZodNullable, $ZodReadonly) must pass through internal properties from their inner type using util.defineLazy for propValues, values, optin, and optout

Applied to files:

  • packages/zod/src/v4/core/schemas.ts
📚 Learning: 2025-10-21T17:27:32.477Z
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.477Z
Learning: Applies to packages/zod/src/v4/core/schemas.ts : Define computed internal properties using util.defineLazy() to avoid circular dependencies

Applied to files:

  • packages/zod/src/v4/core/schemas.ts
📚 Learning: 2025-10-21T17:27:32.477Z
Learnt from: CR
PR: colinhacks/zod#0
File: .cursor/rules/zod-internals.mdc:0-0
Timestamp: 2025-10-21T17:27:32.477Z
Learning: Applies to packages/zod/src/v4/core/{schemas.ts,core.ts} : Use the custom constructor system via core.$constructor() and initialize instances with $ZodType.init() when creating schemas

Applied to files:

  • packages/zod/src/v4/core/schemas.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant