SERVER-100172 Add toEJSON() function to serialize to EJSON (#31797)

GitOrigin-RevId: c21938a9eab69c7854e497c1c0863b5206696255
This commit is contained in:
Daniel Tabacaru 2025-02-04 17:32:44 +02:00 committed by MongoDB Bot
parent 520e3777aa
commit 2be3f39d85
4 changed files with 484 additions and 27 deletions

View File

@ -65,6 +65,7 @@ globals:
tojsononeline: true
tostrictjson: true
tojsonObject: true
toEJSON: true
print: true
printjson: true
printjsononeline: true

View File

@ -29,14 +29,24 @@ x = {
};
assertToJson({
fn: () => tojson(x, "", false),
expectedStr:
'{\n\t"x" : null,\n\t"y" : true,\n\t"z" : 123,\n\t"w" : "foo",\n\t"a" : undefined\n}',
expectedStr: `{
"x" : null,
"y" : true,
"z" : 123,
"w" : "foo",
"a" : undefined
}`,
assertMsg: "C1"
});
assertToJson({
fn: () => tojson(x, "", false),
expectedStr:
'{\n\t"x" : null,\n\t"y" : true,\n\t"z" : 123,\n\t"w" : "foo",\n\t"a" : undefined\n}',
expectedStr: `{
"x" : null,
"y" : true,
"z" : 123,
"w" : "foo",
"a" : undefined
}`,
assertMsg: "C2",
logFormat: "json"
});
@ -46,6 +56,23 @@ assertToJson({
assertMsg: "C3",
logFormat: "json"
});
assertToJson({
fn: () => toEJSON(x, "", false),
expectedStr: `{
"x" : null,
"y" : true,
"z" : 123,
"w" : "foo",
"a" : undefined
}`,
assertMsg: "C4"
});
assertToJson({
fn: () => toEJSON(x, "", false),
expectedStr: '{"x":null,"y":true,"z":123,"w":"foo"}',
assertMsg: "C5",
logFormat: "json"
});
x = {
"x": [],
@ -53,12 +80,22 @@ x = {
};
assertToJson({
fn: () => tojson(x, "", false),
expectedStr: '{\n\t"x" : [ ],\n\t"y" : {\n\t\t\n\t}\n}',
expectedStr: `{
"x" : [ ],
"y" : {
}
}`,
assertMsg: "D1"
});
assertToJson({
fn: () => tojson(x, "", false),
expectedStr: '{\n\t"x" : [ ],\n\t"y" : {\n\t\t\n\t}\n}',
expectedStr: `{
"x" : [ ],
"y" : {
}
}`,
assertMsg: "D2",
logFormat: "json"
});
@ -68,6 +105,18 @@ assertToJson({
assertMsg: "D3",
logFormat: "json"
});
assertToJson({
fn: () => toEJSON(x, "", false),
expectedStr: `{
"x" : [ ],
"y" : {
}
}`,
assertMsg: "D4"
});
assertToJson(
{fn: () => toEJSON(x), expectedStr: '{"x":[],"y":{}}', assertMsg: "D5", logFormat: "json"});
// nested
x = {
@ -76,8 +125,25 @@ x = {
};
assertToJson({
fn: () => tojson(x),
expectedStr:
'{\n\t"x" : [\n\t\t{\n\t\t\t"x" : [\n\t\t\t\t1,\n\t\t\t\t2,\n\t\t\t\t[ ]\n\t\t\t],\n\t\t\t"z" : "ok",\n\t\t\t"y" : [\n\t\t\t\t[ ]\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t"foo" : "bar"\n\t\t}\n\t],\n\t"y" : null\n}',
expectedStr: `{
"x" : [
{
"x" : [
1,
2,
[ ]
],
"z" : "ok",
"y" : [
[ ]
]
},
{
"foo" : "bar"
}
],
"y" : null
}`,
assertMsg: "E1"
});
assertToJson({
@ -87,6 +153,35 @@ assertToJson({
assertMsg: "E2",
logFormat: "json"
});
assertToJson({
fn: () => toEJSON(x),
expectedStr: `{
"x" : [
{
"x" : [
1,
2,
[ ]
],
"z" : "ok",
"y" : [
[ ]
]
},
{
"foo" : "bar"
}
],
"y" : null
}`,
assertMsg: "E3"
});
assertToJson({
fn: () => toEJSON(x),
expectedStr: '{"x":[{"x":[1,2,[]],"z":"ok","y":[[]]},{"foo":"bar"}],"y":null}',
assertMsg: "E4",
logFormat: "json"
});
// special types
x = {
@ -95,12 +190,18 @@ x = {
};
assertToJson({
fn: () => tojson(x, "", false),
expectedStr: '{\n\t"x" : ObjectId("4ad35a73d2e34eb4fc43579a"),\n\t"z" : /xd?/gi\n}',
expectedStr: `{
"x" : ObjectId("4ad35a73d2e34eb4fc43579a"),
"z" : /xd?/gi
}`,
assertMsg: "F1"
});
assertToJson({
fn: () => tojson(x, "", false),
expectedStr: '{\n\t"x" : ObjectId("4ad35a73d2e34eb4fc43579a"),\n\t"z" : /xd?/gi\n}',
expectedStr: `{
"x" : ObjectId("4ad35a73d2e34eb4fc43579a"),
"z" : /xd?/gi
}`,
assertMsg: "F2",
logFormat: "json"
});
@ -110,6 +211,20 @@ assertToJson({
assertMsg: "F3",
logFormat: "json"
});
assertToJson({
fn: () => toEJSON(x, "", false),
expectedStr: `{
"x" : ObjectId("4ad35a73d2e34eb4fc43579a"),
"z" : /xd?/gi
}`,
assertMsg: "F4"
});
assertToJson({
fn: () => toEJSON(x),
expectedStr: '{"x":{"$oid":"4ad35a73d2e34eb4fc43579a"},"z":{"$regex":"xd?","$options":"gi"}}',
assertMsg: "F5",
logFormat: "json"
});
// Timestamp type
x = {
@ -117,12 +232,16 @@ x = {
};
assertToJson({
fn: () => tojson(x, "", false),
expectedStr: '{\n\t"x" : Timestamp(0, 0)\n}',
expectedStr: `{
"x" : Timestamp(0, 0)
}`,
assertMsg: "G1"
});
assertToJson({
fn: () => tojson(x, "", false),
expectedStr: '{\n\t"x" : Timestamp(0, 0)\n}',
expectedStr: `{
"x" : Timestamp(0, 0)
}`,
assertMsg: "G2",
logFormat: "json"
});
@ -132,6 +251,19 @@ assertToJson({
assertMsg: "G3",
logFormat: "json"
});
assertToJson({
fn: () => toEJSON(x, "", false),
expectedStr: `{
"x" : Timestamp(0, 0)
}`,
assertMsg: "G4"
});
assertToJson({
fn: () => toEJSON(x),
expectedStr: '{"x":{"$timestamp":{"t":0,"i":0}}}',
assertMsg: "G5",
logFormat: "json"
});
// Timestamp type, second
x = {
@ -139,12 +271,16 @@ x = {
};
assertToJson({
fn: () => tojson(x, "", false),
expectedStr: '{\n\t"x" : Timestamp(10, 2)\n}',
expectedStr: `{
"x" : Timestamp(10, 2)
}`,
assertMsg: "H1"
});
assertToJson({
fn: () => tojson(x, "", false),
expectedStr: '{\n\t"x" : Timestamp(10, 2)\n}',
expectedStr: `{
"x" : Timestamp(10, 2)
}`,
assertMsg: "H2",
logFormat: "json"
});
@ -154,18 +290,63 @@ assertToJson({
assertMsg: "H3",
logFormat: "json"
});
assertToJson({
fn: () => toEJSON(x, "", false),
expectedStr: `{
"x" : Timestamp(10, 2)
}`,
assertMsg: "H4"
});
assertToJson({
fn: () => toEJSON(x),
expectedStr: '{"x":{"$timestamp":{"t":10,"i":2}}}',
assertMsg: "H5",
logFormat: "json"
});
// Map type
x = new Map();
assertToJson({fn: () => tojson(x, "", false), expectedStr: '[ ]', assertMsg: "I"});
assertToJson({fn: () => tojson(x, "", false), expectedStr: '[ ]', assertMsg: "I1"});
assertToJson({fn: () => toEJSON(x, "", false), expectedStr: '[ ]', assertMsg: "I2"});
assertToJson(
{fn: () => toEJSON(x), expectedStr: '{"$map":[]}', assertMsg: "I3", logFormat: "json"});
x = new Map();
x.set("one", 1);
x.set(2, "two");
assertToJson({
fn: () => tojson(x, "", false),
expectedStr: '[\n\t[\n\t\t\"one\",\n\t\t1\n\t],\n\t[\n\t\t2,\n\t\t\"two\"\n\t]\n]',
assertMsg: "J"
expectedStr: `[
[
"one",
1
],
[
2,
"two"
]
]`,
assertMsg: "J1"
});
assertToJson({
fn: () => toEJSON(x, "", false),
expectedStr: `[
[
"one",
1
],
[
2,
"two"
]
]`,
assertMsg: "J2"
});
assertToJson({
fn: () => toEJSON(x),
expectedStr: '{"$map":[["one",1],[2,"two"]]}',
assertMsg: "J3",
logFormat: "json"
});
x = new Map();
@ -173,23 +354,74 @@ x.set("one", 1);
x.set(2, {y: [3, 4]});
assertToJson({
fn: () => tojson(x, "", false),
expectedStr:
'[\n\t[\n\t\t\"one\",\n\t\t1\n\t],\n\t[\n\t\t2,\n\t\t{\n\t\t\t\"y\" : [\n\t\t\t\t3,\n\t\t\t\t4\n\t\t\t]\n\t\t}\n\t]\n]',
expectedStr: `[
[
"one",
1
],
[
2,
{
"y" : [
3,
4
]
}
]
]`,
assertMsg: "K1"
});
assertToJson({
fn: () => tojson(x, "", false),
expectedStr:
'[\n\t[\n\t\t\"one\",\n\t\t1\n\t],\n\t[\n\t\t2,\n\t\t{\n\t\t\t\"y\" : [\n\t\t\t\t3,\n\t\t\t\t4\n\t\t\t]\n\t\t}\n\t]\n]',
expectedStr: `[
[
"one",
1
],
[
2,
{
"y" : [
3,
4
]
}
]
]`,
assertMsg: "K2",
logFormat: "json"
});
assertToJson({
fn: () => tojson(x),
expectedStr: '[ [ \"one\", 1 ], [ 2, { \"y\" : [ 3, 4 ] } ] ]',
expectedStr: '[ [ "one", 1 ], [ 2, { "y" : [ 3, 4 ] } ] ]',
assertMsg: "K3",
logFormat: "json"
});
assertToJson({
fn: () => toEJSON(x, "", false),
expectedStr: `[
[
"one",
1
],
[
2,
{
"y" : [
3,
4
]
}
]
]`,
assertMsg: "K4"
});
assertToJson({
fn: () => toEJSON(x),
expectedStr: '{"$map":[["one",1],[2,{"y":[3,4]}]]}',
assertMsg: "K5",
logFormat: "json"
});
assert.eq(x, x);
assert.neq(x, new Map());
@ -199,6 +431,104 @@ y.set("one", 1);
y.set(2, {y: [3, 4]});
assert.eq(x, y);
// Set type
x = new Set([{"x": [1, 2, []], "z": "ok", "y": [[]]}, {"foo": "bar"}]);
assertToJson({
fn: () => tojson(x, "", false),
expectedStr: `[
{
"x" : [
1,
2,
[ ]
],
"z" : "ok",
"y" : [
[ ]
]
},
{
"foo" : "bar"
}
]`,
assertMsg: "L1"
});
assertToJson({
fn: () => toEJSON(x, "", false),
expectedStr: `[
{
"x" : [
1,
2,
[ ]
],
"z" : "ok",
"y" : [
[ ]
]
},
{
"foo" : "bar"
}
]`,
assertMsg: "L2"
});
assertToJson({
fn: () => toEJSON(x),
expectedStr: '{"$set":[{"x":[1,2,[]],"z":"ok","y":[[]]},{"foo":"bar"}]}',
assertMsg: "L3",
logFormat: "json"
});
// Array type
x = new Array({"x": [1, 2, []], "z": "ok", "y": [[]]}, {"foo": "bar"});
assertToJson({
fn: () => tojson(x, "", false),
expectedStr: `[
{
"x" : [
1,
2,
[ ]
],
"z" : "ok",
"y" : [
[ ]
]
},
{
"foo" : "bar"
}
]`,
assertMsg: "M1"
});
assertToJson({
fn: () => toEJSON(x, "", false),
expectedStr: `[
{
"x" : [
1,
2,
[ ]
],
"z" : "ok",
"y" : [
[ ]
]
},
{
"foo" : "bar"
}
]`,
assertMsg: "M2"
});
assertToJson({
fn: () => toEJSON(x),
expectedStr: '[{"x":[1,2,[]],"z":"ok","y":[[]]},{"foo":"bar"}]',
assertMsg: "M3",
logFormat: "json"
});
// tostrictjson produces proper output
x = {
"x": NumberLong(64)
@ -227,16 +557,69 @@ x = {
"data_maxkey": MaxKey,
"data_numberlong": NumberLong("12345"),
"data_numberint": NumberInt(5),
"data_numberdecimal": NumberDecimal(3.14)
"data_numberdecimal": NumberDecimal(3.14),
"data_date": new Date(1970, 0, 1, 23, 59, 59, 999)
};
assert.eq(
JSON.stringify(x),
'{"data_binary":{"$binary":"VG8gYmUgb3Igbm90IHRvIGJlLi4uIFRoYXQgaXMgdGhlIHF1ZXN0aW9uLg==","$type":"00"},"data_timestamp":{"$timestamp":{"t":987654321,"i":0}},"data_regex":{"$regex":"^acme","$options":"i"},"data_oid":{"$oid":"579a70d9e249393f153b5bc1"},"data_ref":{"$ref":"test","$id":"579a70d9e249393f153b5bc1"},"data_minkey":{"$minKey":1},"data_maxkey":{"$maxKey":1},"data_numberlong":{"$numberLong":"12345"},"data_numberint":5,"data_numberdecimal":{"$numberDecimal":"3.14000000000000"}}');
'{"data_binary":{"$binary":"VG8gYmUgb3Igbm90IHRvIGJlLi4uIFRoYXQgaXMgdGhlIHF1ZXN0aW9uLg==","$type":"00"},"data_timestamp":{"$timestamp":{"t":987654321,"i":0}},"data_regex":{"$regex":"^acme","$options":"i"},"data_oid":{"$oid":"579a70d9e249393f153b5bc1"},"data_ref":{"$ref":"test","$id":"579a70d9e249393f153b5bc1"},"data_minkey":{"$minKey":1},"data_maxkey":{"$maxKey":1},"data_numberlong":{"$numberLong":"12345"},"data_numberint":5,"data_numberdecimal":{"$numberDecimal":"3.14000000000000"},"data_date":"1970-01-01T23:59:59.999Z"}');
assertToJson({
fn: () => toEJSON(x),
expectedStr:
'{"data_binary":{"$binary":"VG8gYmUgb3Igbm90IHRvIGJlLi4uIFRoYXQgaXMgdGhlIHF1ZXN0aW9uLg==","$type":"00"},"data_timestamp":{"$timestamp":{"t":987654321,"i":0}},"data_regex":{"$regex":"^acme","$options":"i"},"data_oid":{"$oid":"579a70d9e249393f153b5bc1"},"data_ref":{"$ref":"test","$id":"579a70d9e249393f153b5bc1"},"data_minkey":{"$minKey":1},"data_maxkey":{"$maxKey":1},"data_numberlong":{"$numberLong":"12345"},"data_numberint":5,"data_numberdecimal":{"$numberDecimal":"3.14000000000000"},"data_date":{"$date":"1970-01-01T23:59:59.999+00:00"}}',
assertMsg: "N1",
logFormat: "json"
});
// Serialize recursive object
x = {};
x.self = x;
assertToJson({
fn: () => toEJSON(x),
expectedStr: '{"self":"[recursive]"}',
assertMsg: "N2",
logFormat: "json"
});
// Serialize containers
x = {};
x.array = new Array(1, "two", [3, false]);
x.set = new Set([1, "two", true]);
x.map = new Map([["one", 1], [2, {y: [3, 4]}]]);
assertToJson({
fn: () => toEJSON(x),
expectedStr:
'{"array":[1,"two",[3,false]],"set":{"$set":[1,"two",true]},"map":{"$map":[["one",1],[2,{"y":[3,4]}]]}}',
assertMsg: "N3",
logFormat: "json"
});
// serializing Error instances
// Serialize Error instances
const stringThatNeedsEscaping = 'ho\"la';
assert.eq('\"ho\\\"la\"', JSON.stringify(stringThatNeedsEscaping));
assert.eq('\"ho\\\"la\"', tojson(stringThatNeedsEscaping));
assertToJson(
{fn: () => tojson(stringThatNeedsEscaping), expectedStr: '\"ho\\\"la\"', assertMsg: "O1"});
assertToJson(
{fn: () => toEJSON(stringThatNeedsEscaping), expectedStr: '\"ho\\\"la\"', assertMsg: "O2"});
assertToJson({
fn: () => toEJSON(stringThatNeedsEscaping),
expectedStr: '\"ho\\\"la\"',
assertMsg: "O3",
logFormat: "json"
});
assert.eq('{}', JSON.stringify(new Error(stringThatNeedsEscaping)));
assert.eq('Error(\"ho\\\"la\")', tojson(new Error(stringThatNeedsEscaping)));
assertToJson({
fn: () => tojson(new Error(stringThatNeedsEscaping)),
expectedStr: 'Error(\"ho\\\"la\")',
assertMsg: "O5"
});
assertToJson({
fn: () => toEJSON(new Error(stringThatNeedsEscaping)),
expectedStr: 'Error(\"ho\\\"la\")',
assertMsg: "O6"
});
assertToJson({
fn: () => toEJSON(new Error(stringThatNeedsEscaping)),
expectedStr: '{"$error":"ho\\\"la"}',
assertMsg: "O7",
logFormat: "json"
});

View File

@ -119,6 +119,7 @@ const expectedGlobalVars = [
"tojsonObject",
"tojsononeline",
"tostrictjson",
"toEJSON",
"undefined",
"unescape",
"version",

View File

@ -656,10 +656,27 @@ if (typeof (gc) == "undefined") {
}
// Free Functions
/**
* Functions to serialize to JSON and EJSON.
*
* 'tojson()' and 'tojsononeline()' serialize to JSON. The purpose of these functions is to preserve
* compatibility with 'eval()'. The output produced by tojson()/tojsononeline() can be used by
* 'eval()' to deserialize the object (note: eval does not work with EJSON). Use in any non-logging
* code.
*
* 'toEJSON()' is similar to 'tostrictjson()' (and both serialize to EJSON), but it works
* on any type (not just BSON objects) and also fixes some of its limitations (ie, circular
* dependencies). The purpose of this function is to to make sure log lines are parsable by
* non-mongo-shell json parsing tools like Parsely. Use in any logging code.
*/
// Note: use 'toEJSON()' instead if the output is used for assertions and/or logging.
tojsononeline = function(x) {
return tojson(x, " ", true);
};
// Note: use 'toEJSON()' instead if the output is used for assertions and/or logging.
tojson = function(x, indent, nolint, depth, sortKeys) {
if (x === null)
return "null";
@ -785,6 +802,61 @@ tojsonObject = function(x, indent, nolint, depth, sortKeys) {
return s + indent + "}";
};
// Converts a JavaScript value to an EJSON string when 'TestData.logFormat == "json"', otherwise
// returns the same result as 'tojson()'.
// Compared to standard EJSON, this function outputs a different format for Sets, Maps, and Errors.
// * Sets serialize as {"$set": [<value1>,...]}
// * Maps serialize as {"$map": [[<key1>, <value1>],...]}
// * Errors serialize as {"$error": "<error_message>"}
//
// Use this function in assertions and/or logging.
// Note: Use 'tojson()' or 'tojsononeline()' instead if the output is used to rehydrate objects
// using 'eval()'.
toEJSON = function(x, indent, nolint, depth, sortKeys) {
if (typeof TestData !== "object" || TestData.logFormat !== "json")
return tojson(x, indent, nolint, depth, sortKeys);
function ensureEJSONAndStopOnRecursion() {
// Stack of ancestors (objects) of the current 'value'.
// eg, For {"x": 1, "y": {"z": 2}} and value = 2,
// ancestors = [{"x": 1, "y": {"z": 2}}, {"z": 2}]
let ancestors = [];
return function(key, value) {
if (value instanceof Error) {
return {"$error": value.message};
}
if (value instanceof Map) {
return {"$map": [...value]};
}
if (value instanceof Set) {
return {"$set": [...value]};
}
// 'value' is a a pre-transformed property value (of type string in case of Dates),
// so we use 'this[key]' instead to get the original value.
if (this[key] instanceof Date) {
return {"$date": this[key].toISOString().replace("Z", "+00:00")};
}
if (typeof value !== "object" || value === null) {
return value;
}
// Remove ancestors not part of the path to current 'value' anymore.
// `this` is the object that value is contained in,
// i.e., its direct parent.
while (ancestors.length > 0 && ancestors.at(-1) !== this) {
ancestors.pop();
}
// 'value' is an object at this point. If it has already been seen, prune the traversal
// to avoid a 'TypeError' due to self-referencing objects.
if (ancestors.includes(value)) {
return "[recursive]";
}
ancestors.push(value);
return value;
};
}
return JSON.stringify(x, ensureEJSONAndStopOnRecursion());
};
printjson = function(x) {
print(tojson(x));
};