feat(zod): enforce literal true for select/include/omit options + exclude relation fields from makeModelSchema by default#2525
Conversation
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.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughmakeModelSchema() no longer includes relation fields by default; select/include/omit options are narrowed to accept only the literal Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🟠 MajorPre-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 inBuildIncludeOmitShapecauseskeyof Oto include all declared scalar field keys. This makesField extends keyof Otrue for every field, mapping them all toneverand 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 usernameThe 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 rejectedfalseflags.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 fewas anycases here would keeprawOptionsSchemafrom 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
📒 Files selected for processing (4)
BREAKINGCHANGES.mdpackages/zod/src/factory.tspackages/zod/src/types.tspackages/zod/test/factory.test.ts
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. Useincludeorselectoptions to explicitly opt in to relation fields.Summary by CodeRabbit