Skip to content

fix(orm): handle cyclic JSON typedef references in zod factory (#2654)#2655

Merged
ymc9 merged 1 commit into
devfrom
fix/issue-2654-cyclic-typedef-zod
May 8, 2026
Merged

fix(orm): handle cyclic JSON typedef references in zod factory (#2654)#2655
ymc9 merged 1 commit into
devfrom
fix/issue-2654-cyclic-typedef-zod

Conversation

@ymc9
Copy link
Copy Markdown
Member

@ymc9 ymc9 commented May 8, 2026

Summary

  • makeTypeDefSchema previously recursed forever when JSON typedefs referenced each other (or themselves), throwing RangeError: Maximum call stack size exceeded. The @cache() decorator only populates the cache after the method returns, so the recursive call still saw an empty cache.
  • Wrap nested typedef references in z.lazy(() => …) so the inner lookup defers to validation time, by which point the outer build has finished and cached its result. Same pattern already used by makeJsonValueSchema.
  • Added regression test tests/regression/test/issue-2654.test.ts covering cyclic (AB) and self-referencing (Tree { children Tree[]? }) typedefs.

Fixes #2654

Test plan

  • tests/regression/test/issue-2654.test.ts passes
  • tests/regression/test/issue-{493,558,586}.test.ts (existing JSON typedef coverage) still pass
  • tests/e2e/orm/client-api/typed-json-fields.test.ts still passes

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Resolved infinite recursion errors that occurred during schema generation when working with cyclic or self-referential JSON typedefs, enabling applications to properly create, persist, and retrieve complex nested data structures with circular references.
  • Tests

    • Added comprehensive regression tests verifying the correct behavior and persistence of cyclic and self-referential JSON typedef operations.

Cyclic or self-referencing JSON typedefs caused makeTypeDefSchema to
recurse forever — the @cache() decorator only stores the result after
the method returns, so a recursive call back into the same type still
saw an empty cache. Wrap nested typedef references in z.lazy() so the
inner lookup is deferred to validation time, by which point the outer
build has populated the cache.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 8, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1e7f4c78-2c65-449e-bd56-6ba16ec370db

📥 Commits

Reviewing files that changed from the base of the PR and between d1db37c and 17f44cf.

📒 Files selected for processing (2)
  • packages/orm/src/client/zod/factory.ts
  • tests/regression/test/issue-2654.test.ts

📝 Walkthrough

Walkthrough

The PR fixes a stack overflow when creating data for JSON types with cyclic references by wrapping nested typedef field schemas in z.lazy() to defer evaluation. Two regression tests validate the fix for both mutually recursive and self-referential typedef scenarios.

Changes

Cyclic Typedef Schema Generation

Layer / File(s) Summary
Lazy Schema Construction
packages/orm/src/client/zod/factory.ts
Nested typedef field schemas wrapped in z.lazy() instead of eager construction, deferring evaluation to break cycles in recursive typedef definitions.
Regression Tests
tests/regression/test/issue-2654.test.ts
Two integration tests verify cyclic typedef handling: mutually recursive types (A and B referencing each other) and self-referential types (Tree with optional children), both persisting and returning expected shapes.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 Lazy loops no more shall spin,
When typedefs fold and reference in,
A gentle defer breaks the chain,
Cyclic structures now refrain,
From stack overflow's endless pain! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: fixing cyclic JSON typedef reference handling in the Zod factory, which is the core issue being addressed.
Linked Issues check ✅ Passed The code changes fully address the linked issue #2654 by wrapping nested typedef references in z.lazy() to defer schema construction, preventing infinite recursion for cyclic/self-referential JSON typedefs.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing cyclic JSON typedef handling: modifications to makeTypeDefSchema in factory.ts and a focused regression test for the specific issue.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-2654-cyclic-typedef-zod

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.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 8, 2026

Claude finished @ymc9's task —— View job


Code Review for PR #2655

Todo List:

  • Read and analyze the factory.ts changes
  • Review the regression test implementation
  • Assess code quality and potential issues
  • Check performance and security considerations
  • Provide comprehensive review feedback

Review Summary

This PR effectively solves the cyclic typedef reference issue by implementing a deferred evaluation pattern using z.lazy(). The fix is well-designed and follows existing patterns in the codebase.

Code Quality & Implementation ✅

Factory.ts Changes (packages/orm/src/client/zod/factory.ts:1013-1015)

The implementation is clean and follows the established pattern already used in makeJsonValueSchema():

let fieldSchema: ZodType = isTypeDef(this.schema, def.type)
    ? z.lazy(() => this.makeTypeDefSchema(def.type))
    : this.makeScalarSchema(def.type);

Strengths:

  • Consistent pattern: Uses the same z.lazy() approach as makeJsonValueSchema() for handling recursive structures
  • Targeted fix: Only wraps typedef references, not all field types, minimizing performance overhead
  • Clear documentation: Well-written comment explains the purpose and mechanism
  • Preserves caching: The @cache() decorator on makeTypeDefSchema still works effectively since z.lazy() defers the recursive call

Test Coverage ✅

Comprehensive test cases (tests/regression/test/issue-2654.test.ts)

The test file covers both major cyclic scenarios:

  1. Mutual recursion (AB): Tests typedef types that reference each other
  2. Self-recursion (Tree with optional Tree[] children): Tests typedef types that reference themselves

Test quality highlights:

  • Uses realistic nested data structures that would trigger the original stack overflow
  • Tests both creation scenarios that previously failed
  • Follows existing test patterns in the codebase
  • Uses appropriate PostgreSQL provider for JSON field testing

Performance Considerations ✅

  • Minimal overhead: z.lazy() only affects typedef field references, not all schema generation
  • Maintains caching: The @cache() decorator continues to work effectively
  • Deferred evaluation: Schema building overhead is moved to validation time, which is appropriate
  • No breaking changes: Existing non-cyclic typedef schemas work exactly as before

Security Considerations ✅

  • No security implications: This is purely a schema generation fix
  • Input validation preserved: All existing Zod validation rules remain intact
  • No new attack vectors: The fix doesn't introduce any new data handling paths

Potential Issues & Suggestions

Minor Considerations:

  1. Error context: When a lazy-evaluated schema fails validation, the error message might be slightly less clear about which nested typedef caused the issue. However, this is a reasonable tradeoff for fixing the stack overflow.

  2. Memory usage: z.lazy() creates additional function closures, but the impact is negligible compared to fixing the infinite recursion bug.

Code Quality Observations:

The fix is minimal, targeted, and follows established patterns. The implementation correctly identifies that the root cause was the @cache() decorator populating the cache after method completion, not during recursive calls.

Overall Assessment

Excellent fix that:

  • ✅ Solves the reported issue without breaking existing functionality
  • ✅ Uses established patterns from the codebase (makeJsonValueSchema)
  • ✅ Includes comprehensive regression test coverage
  • ✅ Has minimal performance impact
  • ✅ Is well-documented and easy to understand

This PR is ready for merge. The implementation is clean, well-tested, and follows TypeScript/Zod best practices.


@ymc9 ymc9 merged commit 7283d0e into dev May 8, 2026
8 checks passed
@ymc9 ymc9 deleted the fix/issue-2654-cyclic-typedef-zod branch May 8, 2026 03:51
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.

Runtime error when creating data for JSON type with cyclic reference

1 participant