Skip to content

types: avoid FlattenMaps by default on toObject(), toJSON(), lean() #15518

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

Open
wants to merge 8 commits into
base: vkarpov15/schema-create
Choose a base branch
from

Conversation

vkarpov15
Copy link
Collaborator

@vkarpov15 vkarpov15 commented Jul 5, 2025

Re: #13523

Summary

FlattenMaps, BufferToBinary, and similar recursive type transformations can be problematic with user-specified types because they end up transforming custom objects into records with no good workaround.

The example from #13523 still fails. Another example is the following:

  class MyTest {
    name: Buffer;
    constructor(name: string) {
      this.name = Buffer.from(name);
    }
  }
  type Y = { test: MyTest };
  type X = BufferToBinary<Y>;
  // Fails because MyTest's `name` got converted from Buffer to Binary
  const x: X = {} as Y;

In general, I want us to minimize type transformations like this in favor of inferring from the schema definition, which is what Schema.create() does.

With this PR, toObject(), toJSON(), and lean() no longer FlattenMaps or BufferToBinary by default. To support the unfortunate inconsistency that lean() returns Buffers as mongodb.Binary but toObject(), toJSON() etc. use Buffers, I added a TLeanResultType to schemas.

For backwards compatibility, I moved BufferToBinary<> into the schema class-level generics for defining TLeanResultType<>. However, with this PR we won't FlattenMaps by default, so users who put maps in their raw doc type like new Schema<{ myMap: Map<string, string> }> will need to either change from using maps in raw doc type, or set flattenMaps explicitly in toObject() and use lean<FlattenMaps<RawDocType>> for lean queries.

Potential consolidation we could do: make toObject() and toJSON() convert Buffers to mongodb.Binary, in both JS runtime and TypeScript types. This would be a breaking change, and most likely cause some headache for application developers because Binary and Buffer end up with different representations in JSON. But this would reduce the number of types we need to support in TypeScript. WDYT?

$ node
Welcome to Node.js v20.18.1.
Type ".help" for more information.
> const buf = Buffer.from('hello world')
undefined
> buf
<Buffer 68 65 6c 6c 6f 20 77 6f 72 6c 64>
> buf.toJSON()
{
  type: 'Buffer',
  data: [
    104, 101, 108, 108,
    111,  32, 119, 111,
    114, 108, 100
  ]
}
> const Binary = require('mongodb').Binary
undefined
> const bin = new Binary(buf)
undefined
> bin.toJSON()
'aGVsbG8gd29ybGQ='
> 

Examples

@vkarpov15 vkarpov15 requested a review from Copilot July 5, 2025 18:45
@vkarpov15 vkarpov15 marked this pull request as draft July 5, 2025 18:45
Copy link
Contributor

@Copilot 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 changes the default behavior so that Maps are no longer flattened by default in toObject(), toJSON(), and .lean() calls, updating types and tests accordingly.

  • Convert default Map outputs to Record<string, …> in generated types
  • Introduce explicit flattenMaps: true in toJSON() tests
  • Add/adjust tests for .lean() map output and model instance methods

Reviewed Changes

Copilot reviewed 4 out of 10 changed files in this pull request and generated no comments.

File Description
test/types/schema.create.test.ts Updated schema typing from Map to Record<string, …> and added RawDocType3 for object vs. hydrated doc tests
test/types/models.test.ts Imported ObtainSchemaGeneric, changed expectTypeexpectAssignable, and instantiated ProjectModel to test instance methods
test/types/maps.test.ts Modified toJSON() call to include { flattenMaps: true } in map tests
test/types/lean.test.ts Added a new .lean() case verifying that Maps default to Record<string, …>
Comments suppressed due to low confidence (2)

test/types/maps.test.ts:73

  • Consider adding a test for the default toJSON() behavior (without flattenMaps) to ensure map1 remains a Map when flattenMaps is not specified.
  doc.toJSON({ flattenMaps: true }).map1.foo;

test/types/models.test.ts:580

  • Await the Promise returned by doSomething() to prevent unhandled promise rejections, e.g., await doc.doSomething();.
  doc.doSomething();

…anResultType can be computed by the same helper re: #13523
@vkarpov15 vkarpov15 marked this pull request as ready for review July 6, 2025 17:52
Copy link
Collaborator

@hasezoey hasezoey left a comment

Choose a reason for hiding this comment

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

LGTM.

Requires minimal changes to typegoose's tests, once something that was written wrongly by typegoose and once because it explicitly tested against having FlattenMaps in a toJSON().

Is it intentional to target branch vkarpov15/schema-create instead of 9.0?

Potential consolidation we could do: make toObject() and toJSON() convert Buffers to mongodb.Binary, in both JS runtime and TypeScript types. This would be a breaking change, and most likely cause some headache for application developers because Binary and Buffer end up with different representations in JSON. But this would reduce the number of types we need to support in TypeScript. WDYT?

I dont have particular feelings about that, but it seems like it Binary is base64 encoded and so requires less storage in json, but at the same time seemingly does not have a identifier marking it as a Buffer or Binary like Buffer has.
Maybe make it toggle-able via similar options to flattenObjectIds / flattenMaps?

@@ -256,23 +256,34 @@ declare module 'mongoose' {
set(value: string | Record<string, any>): this;

/** The return value of this method is used in calls to JSON.stringify(doc). */
toJSON(options: ToObjectOptions & { flattenMaps: true, flattenObjectIds: true, virtuals: true }): FlattenMaps<ObjectIdToString<Default__v<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.

Should those options be listed in ToObjectOptions with documentation?

hasezoey added a commit to typegoose/typegoose that referenced this pull request Jul 9, 2025
@hasezoey hasezoey added the typescript Types or Types-test related issue / Pull Request label Jul 9, 2025
@vkarpov15
Copy link
Collaborator Author

Yes, intentional to branch off of schema-create branch. There's some stuff in this PR that relies on schema-create changes.

For now, I'll hold off on making toObject() and toJSON() convert buffer to binary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
typescript Types or Types-test related issue / Pull Request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants