Skip to content

Conversation

@AbdelrahmanHafez
Copy link
Collaborator

Summary

  • Add flattenUUIDs option to toObject() and toJSON() that converts UUID instances to 36-character hex strings, similar to flattenObjectIds
  • Add TypeScript support with UUIDToString<T> type helper and method overloads
  • Rename UUIDToJSON to UUIDToString for consistency with ObjectIdToString, keeping UUIDToJSON as a deprecated alias for backwards compatibility

Fixes #15021

Copy link
Contributor

Copilot AI left a 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 flattenUUIDs option to ToObjectOptions interface and document transformation methods
  • Implemented UUID-to-string conversion logic in the clone helper
  • Renamed UUIDToJSON type to UUIDToString with 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.

Copy link
Collaborator

@vkarpov15 vkarpov15 left a 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 😟

toJSON(options: ToObjectOptions & { flattenMaps: true }): FlattenMaps<Require_id<DocType>>;

// flattenUUIDs overloads
toJSON(options: ToObjectOptions & { flattenUUIDs: true, virtuals: true }): UUIDToString<Require_id<DocType & TVirtuals>>;
Copy link
Collaborator

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: true
  • flattenUUIDs: true, flattenObjectIds: true
  • flattenUUIDs: true, flattenObjectIds: true, flattenMaps: true
  • flattenUUIDs: true, flattenObjectIds: true, flattenMaps: true, virtuals: true
  • flattenUUIDs: true, flattenObjectIds: true, virtuals: true
  • flattenUUIDs: 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.

Copy link
Collaborator Author

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 pass

These 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
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is rename necessary?

Copy link
Collaborator Author

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.

Copy link
Collaborator

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> =
Copy link
Collaborator Author

@AbdelrahmanHafez AbdelrahmanHafez Dec 22, 2025

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)

Copy link
Collaborator

@vkarpov15 vkarpov15 left a 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 } },
Copy link
Collaborator

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
Copy link
Collaborator

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;
Copy link
Collaborator

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 = {}> =
Copy link
Collaborator

Choose a reason for hiding this comment

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

image

@vkarpov15 vkarpov15 added this to the 9.2 milestone Jan 4, 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.

flattenUUIDs

4 participants