javascripttypesjson-deserialization

Serialise BigDecimal, BigNumber, BigInt etc. to and from JSON


What types, and what serialisation pattern, can I use to ensure I have arbitrary-precision numbers in my JavaScript program while it's running, but the state is serialised to / deserialised from JSON with a minimum of pain?

I am writing an application that makes heavy use of numbers with higher precision than allowed by the ECMAScript native Number type. So I need to use a custom type (currently BigNumber) to represent those values.

I am looking hungrily at the proposed BigInt type, which when implemented as standard will be an improvement for many applications including the one I'm writing. However that won't help at all with JSON, which knows nothing about BigInt any more than BigNumber.

Whichever custom type I choose, there are many values of this type throughout the application state. So much that it's worth considering a custom hook in the serialisation / deserialisation layer that will handle transforming it to / from JSON.

Presumably the JSON document will have to represent the values as JSON native types (e.g. Object or String instances). How then, when de-serialising the entire complex application state, to reliably recognise and de-serialise those instances among all the others, to the correct value of the correct BigInt or BigNumber type?

How can I serialise that state, such that any BigNumber (or insert some other arbitrary-precision number type) value reliably survives the serialisation / de-serialisation process, to correct values of the correct type?


Solution

  • If modifying the type (by adding a property to the prototype) is feasible, there is a toJSON hook from JSON.stringify, designed specifically to help custom types collaborate with JSON serialisation.

    If an object being stringified has a property named toJSON whose value is a function, then the toJSON() method customizes JSON stringification behavior: instead of the object being serialized, the value returned by the toJSON() method when called will be serialized.

    So you could add a new method to the BigNumber class:

    BigNumber.prototype.toJSON = function toJSON(key) {
        return {
            _type: 'BigNumber',
            _data: Object.assign({}, this),
        };
    };
    
    state = {
        lorem: true,
        ipsum: "Consecteur non dibale",
        dolor: new BigNumber(107.58),
        sit: { spam: 5, eggs: 6, beans: 7 },
        amet: false,
    };
    serialisedState = JSON.stringify(state);
    console.debug("serialisedState:", serialisedState);
    //  → '{"lorem":true,"ipsum":"Consecteur non dibale","dolor":{"_type":"BigNumber","_data":{"s":1,"e":2,"c":[1,0,7,5,8]}},"sit":{"spam":5,"eggs":6,"beans":7},"amet":false}'
    

    You can then recognise those specific objects when de-serialising, using the reviver parameter of JSON.parse:

    If a reviver is specified, the value computed by parsing is transformed before being returned. Specifically, the computed value and all its properties (beginning with the most nested properties and proceeding to the original value itself) are individually run through the reviver. Then it is called, with the object containing the property being processed as this, and with the property name as a string, and the property value as arguments. [If the return value is not undefined], the property is redefined to be the return value.

    function reviveFromJSON(key, value) {
        let result = value;
        if (
            (typeof value === 'object' && value !== null)
                && (value.hasOwnProperty('_type'))) {
            switch (value._type) {
            case 'BigNumber':
                result = Object.assign(new BigNumber(0), value._data);
            }
        }
        return result;
    }
    
    state = JSON.parse(serialisedState, reviveFromJSON);
    console.debug("state:", state);
    // → { … dolor: BigNumber { s: 1, e: 2, c: [ 1, 0, 7, 5, 8 ] }, … }