Skip to content

feat(validator): support unknownKeys strip mode in object validation#66

Open
robelest wants to merge 1 commit intoget-convex:mainfrom
robelest:feat/unknownKeys-strip-support
Open

feat(validator): support unknownKeys strip mode in object validation#66
robelest wants to merge 1 commit intoget-convex:mainfrom
robelest:feat/unknownKeys-strip-support

Conversation

@robelest
Copy link

@robelest robelest commented Feb 11, 2026

Summary

  • Extends validateValidator to support unknownKeys: "strip" on object validators.
  • Uses strict-first union matching for parity with backend/helpers updates:
    • First matching non-strip member
    • Otherwise first matching strip object member
  • Keeps strip-mode object behavior: unknown fields are removed while type/required-field checks still apply.

Changes

Production (index.ts)

  • Added unknownKeys?: "strict" | "strip" to ValidatorJSON object validators.
  • Updated object validation to defer deletions until validation succeeds (avoids union branch mutation side effects).
  • Updated union validation to two passes (strict-first, then strip objects) for deterministic matching.

Tests

  • convex/argumentsValidation.ts
    • Added union-object fixtures with different member orderings.
  • convex/argumentsValidation.test.ts
    • Added union precedence tests:
      • strict member preferred over strip member
      • declaration order among strip members
      • reordered strip members preserve more fields
    • Kept existing strip-mode argument tests for query/mutation/action.
  • convex/schemaValidation.test.ts
    • Existing strip-mode insert/patch/replace/type tests retained.

Context

This supports the v.object({ ... }, { unknownKeys: "strip" }) feature from get-convex/convex-backend#348, aligned with corresponding helpers changes.

Note: tests monkey-patch serialized validator JSON to include unknownKeys: "strip" because the currently published convex package does not emit this property yet.

Summary by CodeRabbit

  • New Features

    • Validators can now strip unknown object keys during validation, prefer strict union members before applying strip behavior, and still enforce field type checks.
    • Public validator type updated to allow an object variant with optional unknownKeys: "strict" | "strip".
  • Tests

    • Added comprehensive tests and test utilities for argument and schema validation with strip-mode, including unions and nested scenarios.

@coderabbitai
Copy link

coderabbitai bot commented Feb 11, 2026

📝 Walkthrough

Walkthrough

Adds support for stripping unknown object keys during validation via a new unknownKeys?: "strict" | "strip" on object validators, updates union/object validation to defer deletions and try non-strip members first, adds server functions exercising strip-mode args, and expands tests/utilities for strip behavior (including nested/union cases).

Changes

Cohort / File(s) Summary
Validator Core
index.ts
Add `unknownKeys?: "strict"
Server API: arguments validation
convex/argumentsValidation.ts
Add exported functions exercising strip-mode args and union ordering: queryWithStripObjectArg, mutationWithStripObjectArg, actionWithStripObjectArg, queryWithUnionStripNarrowFirstArg, queryWithUnionStripWideFirstArg, and queryWithUnionNestedStripFailureArg.
Arguments tests & utilities
convex/argumentsValidation.test.ts
Introduce test utilities (patchExportArgs, forceStripUnknownKeys, forceStripUnknownKeysOnUnionObjectField, forceStripUnknownKeysOnNestedUnionObjectField) and extensive tests covering strip-mode for query/mutation/action args, union-object member preference, ordering effects, nested failures, and restoration behavior.
Schema tests
convex/schemaValidation.test.ts
Add schemaWithStripUnknownKeys helper and tests verifying extra fields are stripped on insert/patch/replace while type validation still fails for invalid types.
Misc / Manifest
package.json
Reported as touched in diff metadata (no public API changes described).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Server
    participant Validator
    participant Database

    Client->>Server: call function with object arg (may contain unknown keys)
    Server->>Validator: validate args (object validator, unknownKeys: "strip" or union)
    alt A non-strip union member validates
        Validator-->>Server: success (no deletions applied)
    else Only strip-object member validates
        Validator->>Validator: collect unknown keys, defer deletions
        Validator-->>Server: success (with candidate value)
        Server->>Validator: apply deferred deletions (strip unknown keys)
    end
    Server->>Database: perform operation / use parsed args
    Database-->>Server: result
    Server-->>Client: response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇
I hop through fields both new and old,
I lop off extras, tidy and bold,
Validators hum, unions decide,
I keep the meadow neat inside,
A carrot for clean objects—joy untold! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and concisely describes the main feature being added: support for unknownKeys strip mode in object validation, which aligns with the primary changes across index.ts, test files, and new fixtures.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

@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

🤖 Fix all issues with AI agents
In `@index.ts`:
- Around line 1073-1082: The loop that deletes unexpected keys (for (const k of
Object.keys(value)) { ... delete value[k]; }) mutates the input object in-place
when validator.unknownKeys === "strip", which can corrupt the shared value when
this object validator is tried as one branch of a "union" validator; change the
code to avoid in-place mutation by operating on a shallow copy of value (e.g.,
const stripped = { ...value } and delete keys from stripped) or defer performing
deletions until after all field validations succeed so the original value passed
into the "union" branch remains unmodified; locate the object-validation branch
that reads validator.unknownKeys and the union handling logic to implement the
shallow-copy or deferred-deletion approach.

@robelest robelest force-pushed the feat/unknownKeys-strip-support branch 2 times, most recently from ea58c43 to 569b647 Compare February 17, 2026 23:17
Copy link

@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

🧹 Nitpick comments (1)
convex/argumentsValidation.test.ts (1)

10-59: LGTM — test helpers are well-structured with proper cleanup.

patchExportArgs correctly uses bind to capture the original and the try/finally pattern in every caller ensures restoration. One minor note: the restore() callback assigns an own property (the bound original) rather than deleting the own property to re-expose the prototype chain. This is benign for these tests but not a perfect structural restore — consider delete functionExport.exportArgs as the restore action if the original was a prototype method:

🔧 Optional: cleaner restore that preserves prototype chain
 function patchExportArgs(
   functionExport: { exportArgs: () => string },
   patch: (exported: any) => void,
 ): () => void {
+  const hadOwnProp = Object.prototype.hasOwnProperty.call(functionExport, "exportArgs");
   const originalExportArgs = functionExport.exportArgs.bind(functionExport);
   functionExport.exportArgs = () => {
     const exported = JSON.parse(originalExportArgs());
     patch(exported);
     return JSON.stringify(exported);
   };
   return () => {
-    functionExport.exportArgs = originalExportArgs;
+    if (hadOwnProp) {
+      functionExport.exportArgs = originalExportArgs;
+    } else {
+      delete (functionExport as any).exportArgs;
+    }
   };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/argumentsValidation.test.ts` around lines 10 - 59, The restore
callback returned by patchExportArgs currently reassigns the bound
originalExportArgs as an own property which hides any prototype method; change
the restore logic in patchExportArgs to delete functionExport.exportArgs instead
of assigning originalExportArgs so the original prototype chain is preserved
(ensure this change is applied where patchExportArgs is used by
forceStripUnknownKeys and forceStripUnknownKeysOnUnionObjectField).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@convex/argumentsValidation.ts`:
- Around line 69-88: The calls to v.object(..., { unknownKeys: "strip" }) are
using an unsupported second parameter; update queryWithStripObjectArg,
mutationWithStripObjectArg, and actionWithStripObjectArg to remove the {
unknownKeys: "strip" } argument (i.e., call v.object({ a: v.number() }) ), or if
you need lenient validation replace the schema with v.any() or
v.record(v.string(), v.any()) as appropriate so the handler receives the
expected shape without type errors.

In `@index.ts`:
- Around line 1031-1065: The current tryValidate in the "union" case allows
nested strip-object validations to mutate the original value prematurely; change
tryValidate to operate on a deep clone when the union member is (or contains) an
object with unknownKeys === "strip": detect if member.type === "object" &&
member.unknownKeys === "strip" or if member's schema contains nested
strip-objects, then call validateValidator on structuredClone(value) instead of
value; if that validation succeeds, copy the validated/stripped result back into
the original (e.g., replace the object's properties or assign the clone),
otherwise discard the clone so failed branch attempts do not mutate the
original. Ensure you update the tryValidate function and keep using
validateValidator, validator.value, and the unknownKeys === "strip" checks as
the identifying points.

---

Nitpick comments:
In `@convex/argumentsValidation.test.ts`:
- Around line 10-59: The restore callback returned by patchExportArgs currently
reassigns the bound originalExportArgs as an own property which hides any
prototype method; change the restore logic in patchExportArgs to delete
functionExport.exportArgs instead of assigning originalExportArgs so the
original prototype chain is preserved (ensure this change is applied where
patchExportArgs is used by forceStripUnknownKeys and
forceStripUnknownKeysOnUnionObjectField).

@robelest robelest force-pushed the feat/unknownKeys-strip-support branch from 569b647 to ed552c4 Compare February 17, 2026 23:47
@robelest
Copy link
Author

robelest commented Feb 17, 2026

Addressed the latest CodeRabbit feedback in commit ed552c4:

  • removed unsupported v.object(..., { unknownKeys: "strip" }) fixture usage and kept strip behavior via exportArgs monkey-patching
  • fixed nested strip-mutation leakage in union branch attempts by validating against deep clones when strip-object validators are present, then copying back only on success
  • added regression test for nested strip mutation leakage
  • applied the optional helper restore cleanup (prototype-aware restore path)

Copy link

@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)
index.ts (1)

968-984: target.push(...source) can overflow the call stack for large arrays.

Spreading a large array into push passes every element as a separate function argument, which exhausts the stack limit for arrays beyond ~65 k elements (engine-dependent). Replace with a loop or in-place fill:

♻️ Safer array replacement
 if (Array.isArray(target) && Array.isArray(source)) {
   target.length = 0;
-  target.push(...source);
+  for (const item of source) {
+    target.push(item);
+  }
   return;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.ts` around lines 968 - 984, The array copy in
copyValidatedValueIntoTarget uses target.push(...source) which can overflow the
call stack for very large source arrays; replace the spread push with a safe
in-place copy (e.g., clear target and either push in chunks or use a for loop to
push each element, or assign by index) so large arrays are handled without
creating a huge argument list; update the block handling Array.isArray(target)
&& Array.isArray(source) to iterate or chunk-push instead of using the spread
operator.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@index.ts`:
- Around line 968-984: The array copy in copyValidatedValueIntoTarget uses
target.push(...source) which can overflow the call stack for very large source
arrays; replace the spread push with a safe in-place copy (e.g., clear target
and either push in chunks or use a for loop to push each element, or assign by
index) so large arrays are handled without creating a huge argument list; update
the block handling Array.isArray(target) && Array.isArray(source) to iterate or
chunk-push instead of using the spread operator.

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