How to serialize and deserialize objects containing Bigint values in Javascript?

Asked

Viewed 204 times

5

Let’s adopt any example code that contains a type value Bigint:

const params = { largeNum: 54740991n }

If I try to serialize you with JSON.stringify an error is returned:

const params = { largeNum: 54740991n }

JSON.stringify(params)

Uncaught Typeerror: Do not know how to serialize Bigint

JSON.stringify({ theBiggestInt: 1n, status: true })

  • It is possible to serialize an object that contains a property (whether or not nested in the object) of type Bigint?
  • If yes, how to deserialize?
  • 3

    Wow.... why the negative?? Where can I improve the question?

  • 2

    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.

  • 1

    @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.

  • 2

    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)

  • 1

    @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 for startsWith 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 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 the indexOf was almost twice as fast as the startsWith, 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" the JSON.stringify is dispensable will get a sensational response

  • @Guilhermenascimento I updated the answer. I will still review it later, because I think some ideas can still be developed better...

Show 2 more comments

3 answers

7


There is no "standard" way to do this serialization.

The most trivial way around this problem is to use the second argument from JSON.stringify, which receives a function to modify the way the data is serialized.

An example, quite simplistic, that converts the bigint string is this:

const obj = { data: 1n };
const json = JSON.stringify(obj, (key, val) =>
  typeof val === 'bigint' ? val.toString() : val
);
console.log(json);

The problem with this is that once the value has been converted to a string, the original data type is lost in serialization.

As there is no widespread pattern on as serialize this type of data not natively supported by JSON.stringify, a internal convention to your project to determine how this serialization should be done, so as to store not only the string representing the bigint, but also information that this is a bigint. In this way, the problem of information loss is avoided by simply converting the big int string.

An example is to annotate the string with a magic prefix as __bigint__:, so that the big int 5n would be serialized as the string "__bigint__:5".

Another way, perhaps more practical, is to use an object to encode not just the string representing the bigint, but also its kind. So:

const obj = { data: 5n };
const json = JSON.stringify(obj, (_, val) => {
  if (typeof val === 'bigint') {
    return { $$type: 'bigint', $$str: val.toString() };
  }
  
  return val;
});
console.log(json);

I used the names $$type and $$str to avoid conflicts with other objects that might have properties type and str. Although unlikely, this demonstrates the importance of defining a good convention to serialize this type of data; aiming, of course, to avoid conflicts with existing standards.

And to deserialize to do the reverse way. In this case, one must make use of the second argument of JSON.parse to treat the special object (or alternative form of serialization) previously agreed. Example:

const json = '{"data":{"$$type":"bigint","$$str":"5"}}';

const obj = JSON.parse(json, (_, value) => {
  if (
    typeof value === 'object' &&
    value.$$type === 'bigint' &&
    typeof value.$$str === 'string'  
  ) {
    return BigInt(value.$$str);
  }
  
  return value;
});

console.log(obj); //=> { data: 5n }
                  // tem que olhar pelo console do navegador, o snippet não exibe propriamente


The intention is to give an idea because, as demonstrated by another excellent response, can be very creative here. But I must reiterate the importance of agreeing, internally, a standard and ensure that the new serialization format does not conflict with other use cases.

It is worth remembering that this technique is worth not only to serialize bigint. These same steps can be applied to serialize any type of data that Javascript does not natively support by JSON.stringify.

7

A different approach that did not involve string analysis would be to serialize the value Bigint as an object JSON custom to store, in an array, a version of the value BigInt converted into a numerical basis natively representable by javascript or other language.
The structure suggested above would contain the following members:

  • type: indicative of the type of value that this object encapsulates and must be bigint.
  • base: the numerical basis on which the BigInt will be serialized. Here was adopted the value of the constant Number.MAX_SAFE_INTEGER that represents the largest integer securely representable in Javascript.
  • value: an array of integer values of native type Number representing the value of the type BigInt to be serialized on the basis Number.MAX_SAFE_INTEGER. This array would be with the ordered end second format little-endian that is to say:
    • b | b ∈ N is the numerical basis on which the number will be converted.
    • aₙ | n ∈ N, aₙ < aₙ₊₁ is the coefficient of the numerical basis.
      The value array will have the following format:
      Numero em base numérica arbitrária.
      the array value is the length array m the coefficients of the above formula, whose element index is the respective exponent of the numerical basis b:
      array de coeficientes

let lnum = 256564545784543535678564677898765644n;

function bigIntToJSON(key, val, base= Number.MAX_SAFE_INTEGER) {
  if (typeof val !== "bigint") return val;
  let result = [];
  let b = BigInt(base);
  //Decompõe o número na base numérica.
  for (let n = val; n > 0; n = n / b) {
    result.push(Number(n % b));
  }
  return {
    type: typeof val,
    base: base,
    value: result
  };
}

function JSONToBigInt(key, val) {
  if (typeof val !== 'object') return val;
  if (val.type !== 'bigint') return val;
  //Restaura o número decomposto.
  return val.value.reduce((acc, e, idx) => acc + BigInt(e) * BigInt(val.base) ** BigInt(idx), 0n);
}

//Serializa o número.
let j = JSON.stringify(lnum, bigIntToJSON);
console.log("Número serializado: ", j);

//Desserializa o número.
console.log("Número desserializado: ", JSON.parse(j, JSONToBigInt).toString());

  • 1

    Caraca! this approach was quite interesting. It didn’t even come close to thinking of something like this. In this case the default would be the use of the function JSONToBigInt instead of a string prefixed.

  • 3

    Certainly a way much less gambiarra, which actually solves very well in the matter of "understanding" communication, congratulations! + 1

5

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:

  1. 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.

  • 1

    +1 Work for the answer. Use the prototype It was an interesting hiccup, I hadn’t thought of it. I think in this example, using regex becomes dispensable because in the other answer, regex was about security to ensure that the string only had numbers. Including the function of parse became even simpler....

  • 1

    @Cmtecardeal It is worth remembering that the function of parse trusts blindly in the format (it assumes that JSON will always come with the correct prefix and such). If it is a controlled scenario and you know that the prefix is correctly generated, no problem. Otherwise, she would have to deal with the error cases, and there varies according to each case...

  • Yes, well noticed, there would have to be a pattern to the case. I even clarified this part of your code using a PREFIX , for something like const PREFIX = 'BigInt::' ai in val.substring I would wear BigInt(val.substring(PREFIX.length)).

  • 4

    "if the receiving side "requires" that a JSON is sent and "needs" that it is a Bigint, there is not much way anyway" is well "if" even. If one has control of both sides, using a verbose format like JSON seems to me a little... limited... so to speak.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.