Skip to content

Add v.object() unknownKeys: "strip" mode#348

Open
robelest wants to merge 5 commits intoget-convex:mainfrom
robelest:feat/v.object-optional-validators
Open

Add v.object() unknownKeys: "strip" mode#348
robelest wants to merge 5 commits intoget-convex:mainfrom
robelest:feat/v.object-optional-validators

Conversation

@robelest
Copy link
Copy Markdown
Contributor

@robelest robelest commented Feb 7, 2026

Summary

  • Adds unknownKeys option to v.object() validators, controlling how extra fields are handled
  • "strict" (default): rejects documents with unknown fields (existing behavior)
  • "strip": silently removes unknown fields during validation, before persisting
  • Stripping is applied transparently on document writes (insert/patch/replace/import) and UDF arg validation

Design for future Passthrough

The UnknownKeysMode enum is designed so adding a Passthrough variant is straightforward:

  • strips_unknown_fields() would return false for Passthrough (like Strict — don't modify the document)
  • is_subset already handles mode compatibility via the boolean check
  • check_fields skips the extra-field rejection when strips_unknown_fields() is true — Passthrough would need a similar gate on allows_extra_fields() (re-added at that point)
  • JSON serialization already handles arbitrary string values via FromStr
  • TS type union just needs | "passthrough" added

Backward compatibility

  • Default mode is Strict everywhere — no behavior change for existing schemas
  • JSON format omits unknownKeys field for Strict — old schemas parse correctly
  • TS overloads preserve existing call signatures

Test plan

  • cargo build passes
  • New Rust unit tests for strip_unknown_fields, is_subset mode combinations, UnknownKeysMode parsing
  • 3 new TS tests for v.object unknownKeys option, propagation, and serialization
  • test_scheduled_jobs_garbage_collection is test unrelated to this PR (GC timing issue, zero diff from main)

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@robelest robelest force-pushed the feat/v.object-optional-validators branch from b07bf3e to ad062c7 Compare February 7, 2026 20:55
Copy link
Copy Markdown
Member

@ianmacartney ianmacartney left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

didn't go through all of it, but I think dropping the new type parameter will simplify a lot

Expand<Omit<Type, K>>,
Expand<Omit<Fields, K>>,
IsOptional,
string,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this loses the extracted field paths - maybe we can use the ObjectFieldPaths helper?

Expand<Pick<Fields, K>>,
IsOptional
IsOptional,
string,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

{ [K in keyof Fields]: VOptional<Fields[K]> },
IsOptional
IsOptional,
string,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

Expand<Fields & NewFields>,
IsOptional
IsOptional,
string,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

}[keyof Fields] &
string,
FieldPaths extends string = ObjectFieldPaths<Fields>,
UnknownKeys extends "strict" | "strip" = "strict",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

until we have "passthrough" - I don't think we need this to be reflected in types, which would help with the auto-assigning of FieldPaths (not having to duplicate it or pass "string"). we can cross the "passthrough" bridge later (if ever)

Comment on lines +80 to +81
const validatedArgs = jsonToConvex(validateArgsResult.args[0]) as any;
const functionResult = await functionDefinition.handler(ctx, validatedArgs);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was this a bug before? I don't think this is doing what you want- it'll turn a bigint into { $bigint: "AAAA..." }

Comment on lines +177 to +189
return new VObject<ObjectType<T>, T, "required", any, "strict" | "strip">({
isOptional: "required",
fields,
unknownKeys: options?.unknownKeys ?? "strict",
});
}) as {
<T extends PropertyValidators>(
fields: T,
): VObject<ObjectType<T>, T>;
<T extends PropertyValidators, UK extends "strict" | "strip">(
fields: T,
options: { unknownKeys: UK },
): VObject<ObjectType<T>, T, "required", ObjectFieldPaths<T>, UK>;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hopefully dropping the UnknownKeys type will simplify this

Rust: Convert ObjectValidator tuple to named struct with fields + unknown_keys.
Add UnknownKeysMode enum (Strict, Strip). Strict preserves existing behavior
(reject unknown fields); Strip silently removes them during validation.

- Add check_fields(), check_value(), strip_unknown_fields() methods
- Update is_subset() to account for mode compatibility
- Add strip_value() on DocumentSchema, strip_new_document() on DatabaseSchema
- Wire stripping into transaction.rs (insert/patch/replace), import_facing.rs,
  and UDF arg validation (validation.rs)
- JSON serialization: unknownKeys field omitted for Strict (backward compatible)

TypeScript: Add unknownKeys property to VObject as a concrete runtime type
(not a type parameter). v.object(fields, { unknownKeys: "strip" }) API.
Propagate mode through asOptional, omit, pick, extend, partial.
FieldPaths auto-compute from Fields default (no regression on combinators).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@robelest robelest force-pushed the feat/v.object-optional-validators branch from ad062c7 to b145232 Compare February 8, 2026 20:09
Copy link
Copy Markdown
Member

@ianmacartney ianmacartney left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API looks good.

I want to see more usage of this in JS however - e.g. having some JS that defines these looser object validators as args / returns / schema and runs them "for real" - from Rust-based integration test runner or otherwise.

I'm going to add @Nicolapps to review the Rust side and provide guidance on testing approaches here.

It also makes me realize we should update convex-test to support this around the same time, so folks writing tests see the same behavior. Can you install a local build of convex in convex-test (you can use npm pack to make it) and put up a tentative PR there?

Also convex-helpers has validate and parse functions that should take this into account.

@ianmacartney ianmacartney dismissed their stale review February 10, 2026 01:47

LGTM, deferring to Nicolas from here

Add end-to-end coverage for unknownKeys: "strip" across isolate args validation, application returns validation, and database schema enforcement.

These tests verify that extra object fields are stripped where expected while type validation errors are still raised, and that schema export includes unknownKeys in serialized table definitions.
@robelest
Copy link
Copy Markdown
Contributor Author

robelest commented Feb 20, 2026

Resolved merge conflicts with main and pushed 98489d561.

Preserved the unknownKeys strip behavior and strict-first union matching. Ran targeted isolate/application/database strip-mode tests locally; all passed.

Reformat validator types and tests touched by strip-mode work so formatting checks on PR get-convex#348 pass cleanly.
@Nicolapps Nicolapps removed their request for review April 10, 2026 16:53
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.

2 participants