According to the documentation, JSON.stringify
checks whether the object to be serialized has the method toJSON
(and if so, the result of this is used). We can also see this in language specification:
- If Type(value) is Object or Bigint, then
a. Let toJSON be ? Getv(value, "toJSON").
b. If Iscallable(toJSON) is true, then
i. Set value to ? Call(toJSON, value, « key »).
So a way to serialize a BigInt
would define the method toJSON
in its prototype:
Object.defineProperty(BigInt.prototype, 'toJSON', {
writable: true, configurable: true, enumerable: false,
value: function () {
// converte para uma string em algum formato específico
return 'BigInt::' + this.toString();
}
});
const params = { largeNum: 54740991n };
console.log(JSON.stringify(params)); // {"largeNum":"BigInt::54740991"}
Remembering, of course, of all aspects of changing the prototype of an object. I personally avoid doing so, so I think the solution given in another answer (use a function as the second argument of JSON.stringify
). Anyway, the option is registered.
The other considerations of another answer also remain valid: as the result of serialization is a string, you must choose some specific format to indicate that that is a BigInt
, so that it is possible to convert it back when deserialize. In the above example I added the prefix BigInt::
to number, then to convert back would be so:
const json = '{"largeNum":"BigInt::54740991"}';
const params = JSON.parse(json, function(key, val) {
// se a string começa com o prefixo, pega tudo depois dele
if (typeof val === 'string' && val.startsWith('BigInt::')) {
return BigInt(val.substring(8));
}
return val;
});
console.log(params.largeNum.toString()); // 54740991
console.log(typeof params.largeNum); // bigint
Remembering to choose a format that does not generate ambiguities (for example, if you have other strings that start with BigInt::
but after not having a number, will give problems and will have to choose another prefix, or adjust the function to treat these cases).
Because the function used in the parse
trusts "blindly" in the format used in stringify
. In controlled scenarios, in which you "know" exactly that all strings containing the prefix are BigInt
's, this is not a problem. Otherwise you will have to include the handling of exceptional cases.
It is worth noting that startsWith
is not available in IE, despite having relatively good support in other browsers. If you want more compatibility, you can switch to indexOf
:
const params = JSON.parse(json, function(key, val) {
// se a string começa com o prefixo, pega tudo depois dele
if (typeof val === 'string' && val.indexOf('BigInt::') === 0) {
return BigInt(val.substring(8));
}
return val;
});
Finally, looking more generally, the JSON format (yes, it is a data format, is not unique to Javascript) is agnostic with respect to the types of each language. In format definition there is only number
(numerical literals, such as 42
, 1.25
or 3e19
), and each language maps them to their respective types.
When literals are not enough, another option is to use strings in a specific format - as is done, for example, with dates: usually the date is converted to a specific format, and is in charge of each parser be able to interpret it correctly (hence aberrations may arise as that one).
In the case of BigInt
, the problem is similar. The numeric literals of the JSON format end up being interpreted as the Number
Javascript. Only values above Number.MAX_SAFE_INTEGER
end up giving problem:
const n = JSON.parse('111111111111111111111111111111111111111');
console.log(typeof n); // number
// mas o valor lido não é exatamente o que foi parseado
console.log(n.toLocaleString('pt-BR')); // 111.111.111.111.111.100.000.000.000.000.000.000.000
So one way to solve it would be to actually create the string in a specific format that indicates that the content should be interpreted as BigInt
(as done above). Or treat any string that contains numbers as BigInt
.
But maybe you don’t need all this...
I suggest taking a step back and assessing whether you really need a JSON. I believe you are using JSON because this data should be sent elsewhere (another application/system). If this other place "requires" you to send a JSON and "cannot" change, then there is not much way. But if you can change, it might be interesting to consider other alternatives.
Why not send the values directly? If it is an HTTP request, it could be in itself query string, for example:
http://url.com/api?valorBigInteger=123456789012345678901234567890
And then you send the amount using valorBigInteger.toString()
, and the receiving side treats the string as it sees fit (each language will have its most suitable numeric types for each case).
By the way, is the side that receives JSON also in Javascript? If it is, it needs to be always BigInt
(values less than Number.MAX_SAFE_INTEGER
could not be Number
)? Creating a string with a custom format would be necessary only if the side that receives the data actually needs a BigInt
(or a similar type in the language used) and wants to differentiate it from other numerical types.
Otherwise, sending the data separately and treating them one by one seems the most appropriate. Still using the example above the query string, the application that receives the data knows that a given parameter must be converted to a BigInt
(or whatever the equivalent type in the language of backend), because this is the type expected for that data - so I wouldn’t have to customize the serialization of JSON.
But again, if the receiving side "requires" to send a JSON and "needs" to be a BigInt
(and you have no control over it), there is not much way anyway. But if you can change it, consider other alternatives.
Wow.... why the negative?? Where can I improve the question?
– Cmte Cardeal
Using Regex, for this can be somewhat costly, starting from the premise that a sulfix or prefix solve (until the single moment that solves more or less) a
val.indexOf('prefixo_bigint::') === 0
+val.substring(16)
(i use fixed 16 because we know which "prefix" is desired), imagining that the scenario (which is the majority) of JSON parse is to send large payloads, if the payload sent is small or JSON.stringify will need, could do something own in hand.– Guilherme Nascimento
@Guilhermenascimento Thank you for the observation. I agree with your point, including the hkotsubo solution waives the use of regex and follows an approach similar to your comment.
– Cmte Cardeal
I saw, Edit was after my comment ;) I will keep the comment due to the question of when an implementation like this is needed and when it is not (which is most likely not even)
– Guilherme Nascimento
@Guilhermenascimento I agree that regex is an exaggeration (it almost always is), so I reviewed the answer and blatantly copied his suggestion :-) I just switched
indexOf
forstartsWith
because I seem more semantically suited to the case (or maybe it’s just "personal taste" on my part). And I also agree that depending on the case, it may not even be necessary...– hkotsubo
@hkotsubo I will look at the answer calmly, I just think it’s not really a matter of semantics, but rather of "availability", if this in an environment that supports, modern browsers or Node this great, if you want to run anywhere without worry a
indexOf
will work very well and in the benchmark tests I made theindexOf
was almost twice as fast as thestartsWith
, but I’m not just talking about performance, I’m just adding up "advantages," of course the answer code works. If you can explain about when to "reimplement" theJSON.stringify
is dispensable will get a sensational response– Guilherme Nascimento
@Guilhermenascimento I updated the answer. I will still review it later, because I think some ideas can still be developed better...
– hkotsubo