Skip to content

feat(zod): enforce literal true for select/include/omit options + exclude relation fields from makeModelSchema by default + add "optionality" setting to control runtime optionality of fields#2525

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

Conversation

@marcsigmund
Copy link
Copy Markdown
Contributor

@marcsigmund marcsigmund commented Mar 26, 2026

Summary by CodeRabbit

  • Breaking Changes

    • Schemas no longer include related model fields by default — opt into relations via select/include/omit options that now require literal true for nested entries.
    • Default schemas reject relation fields at runtime unless explicitly included.
  • New Features

    • Added an "optionality" setting to control runtime optionality of fields ("all" vs "defaults").
  • Bug Fixes / Behavior

    • Validation logic now treats missing fields as absent to avoid spurious comparisons/validations for optional/partial inputs.

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

📝 Walkthrough

Walkthrough

makeModelSchema() now omits relation fields by default; select/include/omit options require literal true (or nested option objects for relations) and support an optionality setting. Introduced ABSENT sentinel and optionality handling in runtime/types; tests updated accordingly.

Changes

Cohort / File(s) Summary
Documentation
BREAKINGCHANGES.md
Added breaking-change note: relation fields are excluded by default; must opt-in via select/include.
Type definitions
packages/zod/src/types.ts
Added `ModelSchemaOptionality = 'all'
Runtime / Factory
packages/zod/src/factory.ts
Stricter runtime options schema (literal true / enum); default no-options path now excludes relation fields; added post-processing applyOptionality to wrap fields optionally; select presence drives inclusion.
Runtime / Utils
packages/zod/src/utils.ts
Introduced ABSENT sentinel for missing keys; comparison and many eval guards treat ABSENT as non-applicable (aligned with null/undefined) to avoid validating absent optional fields.
Tests
packages/zod/test/factory.test.ts
Updated tests to reflect relations excluded by default; removed tests relying on falsy select/include; added extensive optionality tests (runtime and type-level) and additional create/update schema coverage.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 I munched the fields beneath the moon,
Relations tucked away till you say "true",
ABSENT hid where empty shadows play,
Optional hops make types more spry,
A tiny twitch — schemas bright and new.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: enforcing literal true for options and excluding relation fields by default, plus adding optionality control.

✏️ 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

Add new `optionality` option to schema factory that controls which fields
are made optional in generated Zod schemas:

- `"all"`: makes every field optional
- `"defaults"`: makes only fields with @default, @updatedat, or already
  optional fields optional
- `undefined`: no changes (default behavior)

The implementation preserves field metadata (descriptions) when wrapping
schemas with `.optional()` to maintain documentation in generated schemas.

This provides more flexibility when generating schemas for partial updates
or forms where certain fields should be optional based on their defaults.
…lSchema

Add extensive test coverage for the `optionality` option in the schema factory's `makeModelSchema` method. Tests cover three modes:

- `optionality: 'all'` - makes all fields optional
- `optionality: 'defaults'` - makes only fields with @default or @updatedat optional
- `optionality: 'required'` - keeps all fields required (default behavior)

Tests verify:
- Field optionality behavior and type inference
- Validation of field constraints when values are provided
- Interaction with other options (omit, select, strict)
- Preservation of metadata (descriptions)
- Handling of computed fields and default values
- Proper validation of required vs optional fields

This ensures the optionality feature works correctly across different scenarios and combinations with other schema options.
…ema methods

Deprecate `makeModelCreateSchema` and `makeModelUpdateSchema` methods in favor of the more flexible `makeModelSchema` method with optionality parameter. Users should now use `makeModelSchema(model, { optionality: 'defaults' })` and `makeModelSchema(model, { optionality: 'all' })` respectively.

This change consolidates the API surface and provides a more consistent interface for schema generation with different optionality requirements.
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.

🧹 Nitpick comments (1)
packages/zod/test/factory.test.ts (1)

1344-1525: Consider consolidating duplicate optionality test blocks.

Lines 1344-1525 (describe('optionality option', ...)) largely duplicate the tests from lines 1193-1342 (describe('optionality', ...)). Both blocks test the same functionality with very similar assertions. Consider merging these into a single comprehensive describe block to reduce maintenance burden and test execution time.

♻️ Suggested consolidation

The two describe blocks can be merged. For example, keep the more descriptive test names from the second block while removing redundant assertions that are already covered in the first block:

-    // ── optionality ─────────────────────────────────────────────────────────
-    describe('optionality option', () => {
-        describe('optionality: "all" — every field becomes optional', () => {
-            it('accepts an empty object when optionality is "all"', () => {
-                // ... duplicate of line 1197-1200
-            });
-            // ... more duplicates
-        });
-    });

Keep unique tests from the second block (e.g., 'makes @default(autoincrement) and @default(now) fields optional on Asset') and merge them into the first block.

🤖 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 1344 - 1525, The tests under
describe('optionality option', ...) duplicate the earlier
describe('optionality', ...) block; consolidate by removing one block and
merging any unique cases: keep a single describe('optionality' or 'optionality
option') block and ensure it contains all unique it(...) tests (e.g., the Asset
`@default`(autoincrement)/@default(now) case) and the
makeModelCreateSchema/makeModelUpdateSchema cases; update references to
factory.makeModelSchema, factory.makeModelCreateSchema and
factory.makeModelUpdateSchema so each unique assertion appears only once and
delete the redundant describe block to avoid duplicated assertions and test
execution.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/zod/test/factory.test.ts`:
- Around line 1344-1525: The tests under describe('optionality option', ...)
duplicate the earlier describe('optionality', ...) block; consolidate by
removing one block and merging any unique cases: keep a single
describe('optionality' or 'optionality option') block and ensure it contains all
unique it(...) tests (e.g., the Asset `@default`(autoincrement)/@default(now)
case) and the makeModelCreateSchema/makeModelUpdateSchema cases; update
references to factory.makeModelSchema, factory.makeModelCreateSchema and
factory.makeModelUpdateSchema so each unique assertion appears only once and
delete the redundant describe block to avoid duplicated assertions and test
execution.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 67ef622e-6a1d-4322-bb38-ec863acfb784

📥 Commits

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

📒 Files selected for processing (4)
  • packages/zod/src/factory.ts
  • packages/zod/src/types.ts
  • packages/zod/src/utils.ts
  • packages/zod/test/factory.test.ts

Consolidate optionality: 'all' tests by removing redundant runtime validation
tests and keeping only type-level inference assertions. The runtime behavior
is already covered by other test suites, so this change reduces duplication
and improves test organization by grouping type inference tests together.
@marcsigmund marcsigmund changed the title feat(zod): enforce literal true for select/include/omit options + exclude relation fields from makeModelSchema by default feat(zod): enforce literal true for select/include/omit options + exclude relation fields from makeModelSchema by default + add "optionality" setting to control runtime optionality of fields Mar 30, 2026
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