diff --git a/src/generics/lib.generics.ts b/src/generics/lib.generics.ts index 322dba2..064ed73 100644 --- a/src/generics/lib.generics.ts +++ b/src/generics/lib.generics.ts @@ -1913,8 +1913,8 @@ namespace Promise { } // JSON.stringify, over JsonValue (src/lib.ts, with JSON.parse). Generic: it is told what it was given by -// the type each call is made with. There is no walking the fields of an object or a class at run time, -// so a class goes by its `toJSON()`, and an object literal or a tuple is a TypeError. +// the type each call is made with. An object literal's or a tuple's fields are walked by `for...in`, which +// the compiler makes for each field of the type; a class has no such walk and goes by its `toJSON()`. namespace JSON { // A value as a JsonValue. R: a signature is read before lib.ts declares JsonValue, a default type // only when the function is used. An else chain: code after an `if` the type settles is still @@ -1949,7 +1949,10 @@ namespace JSON { return toValue(value.toJSON()); } else if (typeof value == "tuple") { - throw new TypeError("JSON.stringify cannot read the fields of an object literal or a tuple: build a JsonValue"); + return fieldsToValue(value); + } else if (typeof value == "object") { + // an object literal with methods + return fieldsToValue(value); } else if (typeof value == "bigint") { throw new TypeError("JSON.stringify cannot serialize a BigInt"); } else { @@ -1957,6 +1960,37 @@ namespace JSON { } } + // An object literal or a tuple. `for...in` is made for each of its fields, `key` a literal, so + // `value[key]` is that field with its own type. A tuple of unnamed fields ("0", "1", ...) is an array, + // as in JavaScript; a function or an undefined field is left out of an object, and is null in an array. + function fieldsToValue(value: T): R { + if ("0" in value) { + const result = JsonValue.newArray(); + for (const key in value) { + if (typeof value[key] == "function") { + result.push(new JsonValue()); + } else { + result.push(toValue(value[key])); + } + } + + return result; + } else { + const result = JsonValue.newObject(); + for (const key in value) { + if (typeof value[key] == "function") { + // left out + } else if (typeof value[key] == "undefined") { + // left out + } else { + result.set(key, toValue(value[key])); + } + } + + return result; + } + } + // up to 10 of them, as in JavaScript function indent(space: S): string { if (typeof space == "string") { diff --git a/tests/json.ts b/tests/json.ts index e527b43..bfff00c 100644 --- a/tests/json.ts +++ b/tests/json.ts @@ -155,6 +155,22 @@ function chained() { assert(JSON.stringify(built) == `{"n":true,"list":["x",null],"empty":{},"added":"y"}`, "set again, chained"); } +// An object literal or a tuple: its fields are known from its type. Stringified first in its program, a +// value whose writing needs other instances of the writer (a number in an object, an array in an array) +// once failed to compile. +function objects() { + assert(JSON.stringify({ x: 1 }) == `{"x":1}`, "an object literal"); + const record = { s: "a\"b", b: true, n: null, list: [1, 2], empty: {} }; + assert(JSON.stringify(record) == `{"s":"a\\"b","b":true,"n":null,"list":[1,2],"empty":{}}`, "every kind of field"); + assert(record.s == "a\"b", "the object is only read"); + assert(JSON.stringify({ outer: { inner: [{ z: "q" }] } }) == `{"outer":{"inner":[{"z":"q"}]}}`, "nested"); + assert(JSON.stringify([{ a: 1 }, { a: 2 }]) == `[{"a":1},{"a":2}]`, "an array of object literals"); + assert(JSON.stringify({ k: 2, f: () => 1 }) == `{"k":2}`, "a function is left out"); + assert(JSON.stringify({ get_started: true, set_id: 5 }) == `{"get_started":true,"set_id":5}`, "names like an accessor's"); + assert(JSON.stringify([1, "a", true]) == `[1,"a",true]`, "a tuple is an array"); + assert(JSON.stringify({ a: 1, b: [2] }, null, 2) == `{\n "a": 1,\n "b": [\n 2\n ]\n}`, "indented"); +} + function roundTrip() { const text = `{"s":"a\\"b\\u00e9","n":[1,-2.5,1e+21],"o":{"t":true,"f":false,"z":null}}`; const again = JSON.stringify(JSON.parse(text)); @@ -163,6 +179,7 @@ function roundTrip() { } function main() { + objects(); parsing(); strings(); rejected();