Skip to content

feat(zod): enforce literal true for select/include/omit options + exclude relation fields from makeModelSchema by default#2525

Open
marcsigmund wants to merge 3 commits intozenstackhq:devfrom
marcsigmund:feat/zod-make-model-schema
Open

feat(zod): enforce literal true for select/include/omit options + exclude relation fields from makeModelSchema by default#2525
marcsigmund wants to merge 3 commits intozenstackhq:devfrom
marcsigmund:feat/zod-make-model-schema

Conversation

@marcsigmund
Copy link
Copy Markdown
Contributor

@marcsigmund marcsigmund commented Mar 26, 2026

refactor(zod): enforce literal true for select/include/omit options

feat(zod): exclude relation fields from makeModelSchema by default
BREAKING CHANGE: makeModelSchema() no longer includes relation fields by default to prevent infinite nesting with circular relations and align with ORM behavior. Use include or select options to explicitly opt in to relation fields.

Summary by CodeRabbit

  • Breaking Changes
    • makeModelSchema() no longer includes relation fields by default — use include or select to opt into related models.
    • Field selection/options now require literal true for inclusion/omission (boolean false/no longer accepted); omit accepts true only.
    • Default schemas now reject relation fields at runtime unless explicitly included.

BREAKING CHANGE: `makeModelSchema()` no longer includes relation fields by default to prevent infinite nesting with circular relations and align with ORM behavior. Use `include` or `select` options to explicitly opt in to relation fields.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 26, 2026

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: 2a4ac9e5-a814-4679-8952-62285db183c8

📥 Commits

Reviewing files that changed from the base of the PR and between ef51472 and 7ff42d4.

📒 Files selected for processing (2)
  • packages/zod/src/factory.ts
  • packages/zod/src/types.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/zod/src/factory.ts

📝 Walkthrough

Walkthrough

makeModelSchema() no longer includes relation fields by default; select/include/omit options are narrowed to accept only the literal true (or nested options for relation fields) and factory/type logic updated to reflect these semantics.

Changes

Cohort / File(s) Summary
Documentation
BREAKINGCHANGES.md
Added breaking-change entry: relation fields are excluded by default; opt-in required via select or include.
Types
packages/zod/src/types.ts
Added internal GetAllModelFieldsShape (scalars + relations). Tightened ModelSchemaOptions so select/include use `true
Runtime / Factory
packages/zod/src/factory.ts
Constrained RawOptions to z.literal(true) or nested option objects; switched to strict object schemas. Default schema generation now excludes relation fields. Removed handling for falsy select/include values; presence of a key implies inclusion.
Tests
packages/zod/test/factory.test.ts
Updated type assertions to reflect relations excluded by default. Added runtime test asserting default schema rejects relation fields. Removed tests that depended on false values in select/include.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐇 I hopped through fields both near and far,
Hid relations now — no more wandering spar,
Only true lets them join the play,
Nested choices guide the way,
I twitch my nose and cheer: refined schema day!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely summarizes the two main changes: enforcing literal true for select/include/omit options and excluding relation fields from makeModelSchema by default.
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 unit tests (beta)
  • Create PR with unit tests

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
Copy Markdown
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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/zod/src/types.ts (1)

313-344: ⚠️ Potential issue | 🟠 Major

Pre-declared options with explicit type annotations produce incorrect schema shapes.

When options are stored in a typed variable before passing to makeModelSchema, the optional mapped type in BuildIncludeOmitShape causes keyof O to include all declared scalar field keys. This makes Field extends keyof O true for every field, mapping them all to never and producing an empty schema that diverges from runtime behavior and the inline literal case.

For example:

const omitOptions: ModelSchemaOptions<Schema, 'User'>['omit'] = { username: true };
const schema = factory.makeModelSchema('User', { omit: omitOptions });
type Result = z.infer<typeof schema>;
// Result type is {} (empty), but runtime schema correctly includes all scalar fields except username

The runtime behavior is correct because rawOptionsSchema.parse() validates the actual object, but the inferred TypeScript type diverges from the runtime schema.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/zod/src/types.ts` around lines 313 - 344, The mapped type in
BuildIncludeOmitShape currently omits a field merely if Field extends keyof O,
which collapses to all declared scalar keys when O is a pre-typed options
object; change the conditional to only omit when the option for that field is
actually true (e.g. Field extends keyof O ? (O[Field] extends true ? never :
Field) : Field) so the type mirrors runtime behavior; update the mapped branch
inside BuildIncludeOmitShape that currently reads "O extends object ? Field
extends keyof O ? never : Field : Field" to perform the value-check against
O[Field] instead.
🧹 Nitpick comments (1)
packages/zod/test/factory.test.ts (1)

121-131: Add regression cases for rejected false flags.

The new assertions cover the default relation behavior, but they no longer pin the other breaking change: { select/include/omit: { field: false } } should throw. A few as any cases here would keep rawOptionsSchema from regressing silently.

Suggested cases
+        it('rejects false flags in select/include/omit', () => {
+            expect(() => factory.makeModelSchema('User', { select: { id: false } } as any)).toThrow();
+            expect(() => factory.makeModelSchema('User', { include: { posts: false } } as any)).toThrow();
+            expect(() => factory.makeModelSchema('User', { omit: { username: false } } as any)).toThrow();
+        });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/zod/test/factory.test.ts` around lines 121 - 131, Add regression
tests to ensure option flags with false values are rejected: update the test
file to call factory.makeModelSchema('User') and assert that raw options like {
select: { posts: false } }, { include: { posts: false } } and { omit: { posts:
false } } (cast as any to bypass TypeScript) produce validation failures via
userSchema.safeParse(...). This targets the rawOptionsSchema behavior and
prevents silent regression of the `{ field: false }` case.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@BREAKINGCHANGES.md`:
- Line 6: Update BREAKINGCHANGES.md to add a second, separate breaking-change
bullet explaining the literal-true option change: state that because
makeModelSchema() now excludes relation fields by default callers that
previously passed non-literal booleans (false or widened boolean types) to
select/include/omit will break, and they must now pass literal true (or narrow
the type) to opt into relations; reference the options by name (select, include,
omit) and the function makeModelSchema() so readers can locate affected call
sites and update their types/arguments accordingly.

In `@packages/zod/src/factory.ts`:
- Around line 52-56: The current schema uses z.object() for nested
rawOptionsSchema which silently strips unknown keys; replace the nested usage
with z.strictObject() for rawOptionsSchema (and any nested schemas used by
select/include entries) so validation rejects unknown keys early — update the
rawOptionsSchema definition referenced by select/include (and ensure any places
constructing rawOptionsSchema for select/include/omit use z.strictObject()
instead of z.object()) so typos like "selsct" are rejected during parsing.

---

Outside diff comments:
In `@packages/zod/src/types.ts`:
- Around line 313-344: The mapped type in BuildIncludeOmitShape currently omits
a field merely if Field extends keyof O, which collapses to all declared scalar
keys when O is a pre-typed options object; change the conditional to only omit
when the option for that field is actually true (e.g. Field extends keyof O ?
(O[Field] extends true ? never : Field) : Field) so the type mirrors runtime
behavior; update the mapped branch inside BuildIncludeOmitShape that currently
reads "O extends object ? Field extends keyof O ? never : Field : Field" to
perform the value-check against O[Field] instead.

---

Nitpick comments:
In `@packages/zod/test/factory.test.ts`:
- Around line 121-131: Add regression tests to ensure option flags with false
values are rejected: update the test file to call
factory.makeModelSchema('User') and assert that raw options like { select: {
posts: false } }, { include: { posts: false } } and { omit: { posts: false } }
(cast as any to bypass TypeScript) produce validation failures via
userSchema.safeParse(...). This targets the rawOptionsSchema behavior and
prevents silent regression of the `{ field: false }` case.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 54ce258a-c6eb-4da0-a455-be9509e682eb

📥 Commits

Reviewing files that changed from the base of the PR and between e79ea0c and ef51472.

📒 Files selected for processing (4)
  • BREAKINGCHANGES.md
  • packages/zod/src/factory.ts
  • packages/zod/src/types.ts
  • packages/zod/test/factory.test.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