Skip to content

fix(NODE-7436): EJSON.stringify type signature#870

Open
chdanielmueller wants to merge 4 commits intomongodb:mainfrom
chdanielmueller:fix-NODE-7436
Open

fix(NODE-7436): EJSON.stringify type signature#870
chdanielmueller wants to merge 4 commits intomongodb:mainfrom
chdanielmueller:fix-NODE-7436

Conversation

@chdanielmueller
Copy link

@chdanielmueller chdanielmueller commented Feb 10, 2026

Description

Summary of Changes

This pull request enhances the flexibility and robustness of the EJSON.stringify function by supporting multiple parameter signature combinations, similar to JSON.stringify.
It introduces a new internal type guard to accurately distinguish between options and replacer parameters, and adds comprehensive tests to ensure correct behavior across all supported overloads.
It supports a previously advertised function signature which was not implemented. EJSON.stringify(object, { relaxed: false }, 2)

Notes for Reviewers

The tests should contain every previously possible and also previously advertised but not working combination of inputs.
The changes within the function have to do with the previous typescript signature which has been used but did not work. See

bson.EJSON.stringify(singleField, { relaxed: false }, 2)

Off Topic: Documented within NODE-7437

I noticed that the array replacement signature for JSON.stringify replaces the serialized keys.

const testDoc = {
  objectId: ObjectId.createFromHexString('111111111111111111111111')
};

const result = EJSON.stringify(testDoc, ['objectId', '$oid']) // {"objectId":{"$oid":"111111111111111111111111"}}
const result2 = EJSON.stringify(testDoc, ['objectId']) // {"objectId":{}}

Maybe if the replacer is an Array it should add the data types to the array.

Release Highlight

Release notes highlight

Double check the following

  • Lint is passing (npm run check:lint)
  • Self-review completed using the steps outlined here
  • PR title follows the correct format: type(NODE-xxxx)[!]: description
    • Example: feat(NODE-1234)!: rewriting everything in coffeescript
  • Changes are covered by tests
  • New TODOs have a related JIRA ticket

@chdanielmueller chdanielmueller requested a review from a team as a code owner February 10, 2026 10:28
@PavelSafronov PavelSafronov added tracked-in-jira There is a ticket in Mongo's Jira instance tracking this issue/PR External Submission labels Feb 10, 2026
@PavelSafronov
Copy link
Contributor

@chdanielmueller , thanks so much for your contribution! The team is going to prioritize this PR and NODE-7436 at our next triage session.

Copy link
Member

@tadjik1 tadjik1 left a comment

Choose a reason for hiding this comment

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

hi @chdanielmueller, thanks a lot for the contribution. The bug is real, well-documented, and the test coverage you added is great.
The core issue is that space = 0 discards the user's space value when options are passed in the replacer position.

However, the fix is significantly more complex than it needs to be. The entire bug can be fixed by deleting one line (space = 0). The existing code already treats any non-array object in replacer position as options.

I would suggest minimal fix:

if (replacer != null && typeof replacer === 'object' && !Array.isArray(replacer)) {
  options = replacer;
  replacer = undefined;
- space = 0
}

The overloads, type guard, and restructured function body are unnecessary for this fix and introduce maintenance overhead.

}

// Check that there are no invalid properties (only known EJSON serialize options)
const validKeys = ['legacy', 'relaxed', 'ignoreUndefined'];
Copy link
Member

Choose a reason for hiding this comment

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

This creates a maintenance footgun with to compiler warning to catch if EJSONSerializeOptions gains a new property, or if detection silently breaks. In this case the new property would be treated as a replacer and passed into JSON.stringify.

Comment on lines +68 to +76
if ('legacy' in value && typeof value.legacy !== 'boolean') {
return false;
}
if ('relaxed' in value && typeof value.relaxed !== 'boolean') {
return false;
}
if ('ignoreUndefined' in value && typeof value.ignoreUndefined !== 'boolean') {
return false;
}
Copy link
Member

Choose a reason for hiding this comment

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

The strict key check causes silent misbehavior on typos. If something like {relaxd: false} will be passed - this guard returns false and the object will silently be forwarded to JSON.stringify as a replacer - producing wrong output, the existing typeof replacer === 'object' && !Array.isArray(replacer) would correctly treat it as options. This is more forgiving and safer.

});
});

context('stringify: Parameter signature combinations', function () {
Copy link
Member

Choose a reason for hiding this comment

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

The test coverage here is great, and these tests are valuable independent of which implementation approach is chosen. Please keep them even with a simplified fix.

if (isEJSONSerializeOptions(replacerOrOptions)) {
options = replacerOrOptions;
replacer = undefined;
space = 0;
Copy link
Member

Choose a reason for hiding this comment

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

I think this is the original bug and removing just this line (avoid unconditional discarding the space value( would make everything work as expected.

* console.log(EJSON.stringify(doc, { relaxed: false }, 2));
* ```
*/
function stringify(
Copy link
Member

Choose a reason for hiding this comment

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

These overloads add type-level documentation of supported call patterns, but they also add maintenance hurdle. Since the runtime checks are needed regardless, and the existing signature with the union type already communicates that options can go in the second position - this doesn't provide enough value to justify the complexity.

@tadjik1 tadjik1 self-assigned this Feb 27, 2026
@tadjik1 tadjik1 added the Primary Review In Review with primary reviewer, not yet ready for team's eyes label Feb 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

External Submission Primary Review In Review with primary reviewer, not yet ready for team's eyes tracked-in-jira There is a ticket in Mongo's Jira instance tracking this issue/PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants