-
-
Notifications
You must be signed in to change notification settings - Fork 4k
feat(document): add flattenUUIDs option to toObject() and toJSON()
#15864
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds a flattenUUIDs option to the toObject() and toJSON() document methods, enabling conversion of UUID instances to 36-character hex strings. This feature mirrors the existing flattenObjectIds functionality and is useful for JSON serialization scenarios where UUID objects need to be represented as strings.
Key Changes
- Added
flattenUUIDsoption toToObjectOptionsinterface and document transformation methods - Implemented UUID-to-string conversion logic in the clone helper
- Renamed
UUIDToJSONtype toUUIDToStringwith backward-compatible alias - Added comprehensive tests for UUID flattening with nested objects, subdocuments, and document arrays
Reviewed changes
Copilot reviewed 5 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| types/index.d.ts | Added flattenUUIDs option to ToObjectOptions interface, implemented UUIDToString<T> type helper, and added deprecated UUIDToJSON alias |
| types/document.d.ts | Added method overloads for toObject() and toJSON() supporting flattenUUIDs option with and without virtuals |
| test/types/document.test.ts | Added TypeScript type tests verifying UUID-to-string conversion behavior |
| test/document.test.js | Added comprehensive JavaScript tests covering UUID flattening in various scenarios including nested objects, subdocuments, and arrays |
| lib/options.js | Added flattenUUIDs: false to default internal options |
| lib/helpers/clone.js | Implemented UUID instance detection and conversion to string when flattenUUIDs option is enabled |
| lib/document.js | Added JSDoc documentation for the new flattenUUIDs option in both toObject() and toJSON() methods |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
vkarpov15
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to make sure the TypeScript types are complete for handling all possible options overrides, which is a pain but it's the right thing to do. Adding all these overrides makes me wonder whether adding this feature is even worthwhile 😟
types/document.d.ts
Outdated
| toJSON(options: ToObjectOptions & { flattenMaps: true }): FlattenMaps<Require_id<DocType>>; | ||
|
|
||
| // flattenUUIDs overloads | ||
| toJSON(options: ToObjectOptions & { flattenUUIDs: true, virtuals: true }): UUIDToString<Require_id<DocType & TVirtuals>>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately we would also need to handle all other combinations:
flattenUUIDs: true, flattenMaps: trueflattenUUIDs: true, flattenObjectIds: trueflattenUUIDs: true, flattenObjectIds: true, flattenMaps: trueflattenUUIDs: true, flattenObjectIds: true, flattenMaps: true, virtuals: trueflattenUUIDs: true, flattenObjectIds: true, virtuals: trueflattenUUIDs: true, flattenMaps: true, virtuals: true
Plus versionKey variants for completeness' sake.
Honestly this is why I don't like adding new options to toObject() and toJSON() unless absolutely necessary: they cause combinatorial explosion in TypeScript. There isn't a good way to handle all the different cases without explicitly listing out every single override.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I reworked this to use compositional conditional types instead of explicit overloads for every combination:
type ApplyVirtuals<T, TVirtuals, O> = O extends { virtuals: true } ? T & TVirtuals : T;
type ApplyVersionKey<T, O, TSchemaOptions> = O extends { versionKey: false } ? Omit<T, VersionKeyName> : T;
type ApplyFlattenTransforms<T, O> = // handles flattenMaps, flattenObjectIds, flattenUUIDs in one passThese feed into ToObjectReturnType<DocType, TVirtuals, O, TSchemaOptions> which composes them all together. Adding a new option now just means adding one more transform type instead of doubling the overload count, so future options won't blow up the type definitions.
The flatten options (flattenMaps, flattenObjectIds, flattenUUIDs) are handled by a single recursive type that processes all three in one traversal. I initially had separate recursive types for each, but nesting them caused TypeScript's "union type too complex" error. The single-pass approach avoids that and also correctly handles edge cases like ObjectIds nested inside Map values.
| * Converts any UUID properties into strings for JSON serialization | ||
| */ | ||
| export type UUIDToJSON<T> = T extends mongodb.UUID | ||
| export type UUIDToString<T> = T extends Types.UUID |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is rename necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rename isn't strictly necessary. I did it for consistency with ObjectIdToString (since we have flattenObjectIds -> ObjectIdToString, it felt natural to have flattenUUIDs -> UUIDToString). The old UUIDToJSON name is preserved as a deprecated alias for backwards compatibility.
That said, I'm happy to drop the rename entirely if you'd prefer to keep things simpler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer to keep the old name, at least unless we're going to drop support for the old name in the next major release. I'd prefer to avoid adding aliased names unless there's a strong reason to - keep the API consistent
…IDs combinations - Reorder overloads from most specific to least specific (4 options -> 3 -> 2 -> 1 -> default) - Add all flattenUUIDs combinations with flattenObjectIds, flattenMaps, and virtuals - Remove redundant explicit false overloads (flattenMaps: false, etc.) - Simplify versionKey: false to single overload instead of 9+ combinations - Add type tests for combined options This addresses the TypeScript combinatorial explosion by using a cleaner overload structure that covers all practical use cases without explicit enumeration of every possible combination.
…n options Replace separate nested transform types with a single-pass ApplyFlattenTransforms type that handles flattenMaps, flattenObjectIds, and flattenUUIDs in one recursive traversal. This approach: 1. Avoids TypeScript's "union type too complex" error that occurred when nesting multiple recursive transform types 2. Correctly handles ObjectIds/UUIDs nested inside Map values, since the single pass recurses into Map values during the same traversal that converts IDs 3. Maintains all existing functionality for virtuals, versionKey, and schema options Key changes: - Remove ApplyFlattenObjectIds, ApplyFlattenUUIDs, ApplyFlattenMaps - Add ApplyFlattenTransforms that handles all three in one pass - Add GetVersionKeyName and update ApplyVersionKey to handle custom version keys - Pass TSchemaOptions through to ToObjectReturnType for proper version key handling - Add tests for ObjectIds/UUIDs inside Map values - Add tests for version key presence when passing options
| type ResolveSchemaOptions<T> = MergeType<DefaultSchemaOptions, T>; | ||
| type TypesAreEqual<A, B> = [A] extends [B] ? [B] extends [A] ? true : false : false; | ||
|
|
||
| type ResolveSchemaOptions<T> = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated ResolveSchemaOptions to preserve type identity when T is {} or DefaultSchemaOptions.
When creating a schema without options (new Schema({...})), TypeScript infers TSchemaOptions as {}. Previously, ResolveSchemaOptions<{}> returned MergeType<DefaultSchemaOptions, {}>, which is structurally identical to DefaultSchemaOptions but a different type expression. This caused expectType checks to fail when comparing inferred document types against manually constructed ones.
Now ResolveSchemaOptions returns DefaultSchemaOptions directly in these cases, so the types are truly identical.
Without this change we need to modify existing tests, see #15864 (comment)
vkarpov15
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple of minor comments but overall LGTM
| } | ||
|
|
||
| const schema = new Schema<DocWithMapOfObjectIds>({ | ||
| userRefs: { type: Map, of: { oderId: Schema.Types.ObjectId } }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor typo - this could be orderId or otherId?
| * Converts any UUID properties into strings for JSON serialization | ||
| */ | ||
| export type UUIDToJSON<T> = T extends mongodb.UUID | ||
| export type UUIDToString<T> = T extends Types.UUID |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer to keep the old name, at least unless we're going to drop support for the old name in the next major release. I'd prefer to avoid adding aliased names unless there's a strong reason to - keep the API consistent
| : unknown; | ||
|
|
||
| type ResolveSchemaOptions<T> = MergeType<DefaultSchemaOptions, T>; | ||
| type TypesAreEqual<A, B> = [A] extends [B] ? [B] extends [A] ? true : false : false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This helper should be equivalent to IfEquals<>, can we reuse IfEquals to avoid adding a separate helper?
| * Computes the return type of toObject/toJSON based on the provided options. | ||
| * Uses a single-pass transform for flatten operations to correctly handle all combinations. | ||
| */ | ||
| export type ToObjectReturnType<DocType, TVirtuals, O extends ToObjectOptions, TSchemaOptions = {}> = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.

Summary
flattenUUIDsoption totoObject()andtoJSON()that converts UUID instances to 36-character hex strings, similar toflattenObjectIdsUUIDToString<T>type helper and method overloadsUUIDToJSONtoUUIDToStringfor consistency withObjectIdToString, keepingUUIDToJSONas a deprecated alias for backwards compatibilityFixes #15021