'Reliable way to check if objects is serializable in JavaScript

Is there a known way or a library that already has a helper for assessing whether an object is serializable in JavaScript?

I tried the following but it doesn't cover prototype properties so it provides false positives:

_.isEqual(obj, JSON.parse(JSON.stringify(obj))

There's another lodash function that might get me closer to the truth, _.isPlainObject. However, while _.isPlainObject(new MyClass()) returns false, _.isPlainObject({x: new MyClass()}) returns true, so it needs to be applied recursively.

Before I venture by myself on this, does anybody know an already reliable way for checking if JSON.parse(JSON.stringify(obj)) will actually result in the same object as obj?



Solution 1:[1]

In the end I created my own method that leverages Underscore/Lodash's _.isPlainObject. My function ended up similar to what @bardzusny proposed, but I'm posting mine as well since I prefer the simplicity/clarity. Feel free to outline pros/cons.

var _ = require('lodash');

exports.isSerializable = function(obj) {
  if (_.isUndefined(obj) ||
      _.isNull(obj) ||
      _.isBoolean(obj) ||
      _.isNumber(obj) ||
      _.isString(obj)) {
    return true;
  }

  if (!_.isPlainObject(obj) &&
      !_.isArray(obj)) {
    return false;
  }

  for (var key in obj) {
    if (!exports.isSerializable(obj[key])) {
      return false;
    }
  }

  return true;
};

Solution 2:[2]

function isSerializable(obj) {
  var isNestedSerializable;
  function isPlain(val) {
    return (typeof val === 'undefined' || typeof val === 'string' || typeof val === 'boolean' || typeof val === 'number' || Array.isArray(val) || _.isPlainObject(val));
  }
  if (!isPlain(obj)) {
    return false;
  }
  for (var property in obj) {
    if (obj.hasOwnProperty(property)) {
      if (!isPlain(obj[property])) {
        return false;
      }
      if (typeof obj[property] == "object") {
        isNestedSerializable = isSerializable(obj[property]);
        if (!isNestedSerializable) {
          return false;
        }
      }
    }
  }
  return true;
}

Recursively iterating over all of given object properties. They can be either:

  • plain objects ("an object created by the Object constructor or one with a [[Prototype]] of null." - from lodash documentation)
  • arrays
  • strings, numbers, booleans
  • undefined

Any other value anywhere within passed obj will cause it to be understood as "un-serializable".

(To be honest I'm not absolutely positive that I didn't omit check for some serializable/non-serializable data types, which actually I think depends on the definition of "serializable" - any comments and suggestions will be welcome.)

Solution 3:[3]

Here is a slightly more Lodashy ES6 version of @treznik solution

    export function isSerialisable(obj) {

        const nestedSerialisable = ob => (_.isPlainObject(ob) || _.isArray(ob))  &&
                                         _.every(ob, isSerialisable);

        return  _.overSome([
                            _.isUndefined,
                            _.isNull,
                            _.isBoolean,
                            _.isNumber,
                            _.isString,
                            nestedSerialisable
                        ])(obj)
    }

Tests

    describe.only('isSerialisable', () => {

        it('string', () => {
            chk(isSerialisable('HI'));
        });

        it('number', () => {
            chk(isSerialisable(23454))
        });

        it('null', () => {
            chk(isSerialisable(null))
        });

        it('undefined', () => {
            chk(isSerialisable(undefined))
        });


        it('plain obj', () => {
            chk(isSerialisable({p: 1, p2: 'hi'}))
        });

        it('plain obj with func', () => {
            chkFalse(isSerialisable({p: 1, p2: () => {}}))
        });


        it('nested obj with func', () => {
            chkFalse(isSerialisable({p: 1, p2: 'hi', n: { nn: { nnn: 1, nnm: () => {}}}}))
        });

        it('array', () => {
            chk(isSerialisable([1, 2, 3, 5]))
        });

        it('array with func', () => {
            chkFalse(isSerialisable([1, 2, 3, () => false]))
        });

        it('array with nested obj', () => {
            chk(isSerialisable([1, 2, 3, { nn: { nnn: 1, nnm: 'Hi'}}]))
        });

        it('array with newsted obj with func', () => {
            chkFalse(isSerialisable([1, 2, 3, { nn: { nnn: 1, nnm: () => {}}}]))
        });

    });

}

Solution 4:[4]

Here's how this can be achieved without relying on 3rd party libraries.

We would usually think of using the typeof operator for this kind of task, but it can't be trusted on its own, otherwise we end up with nonsense like:

typeof null === "object" // true
typeof NaN === "number" // true

So the first thing we need to do is find a way to reliably detect the type of any value (Taken from MDN Docs):

const getTypeOf = (value: unknown) => {
  return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
};

We can then traverse the object or array (if any) recursively and check if the deserialized output matches the input type at every step:

const SERIALIZATION_ERROR = new Error(
  `the input value could not be serialized`
);

const serialize = (input: unknown) => {
  try {
    const serialized = JSON.stringify(input);
    const inputType = getTypeOf(input);

    const deserialized = JSON.parse(serialized);
    const outputType = getTypeOf(parsed);

    if (outputType !== inputType) throw SERIALIZATION_ERROR;

    if (inputType === "object") {
      Object.values(input as Record<string, unknown>).forEach((value) =>
        serialize(value)
      );
    }

    if (inputType === "array") {
      (input as unknown[]).forEach((value) => serialize(value));
    }

    return serialized;
  } catch {
    throw SERIALIZATION_ERROR;
  }
};

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 treznik
Solution 2
Solution 3
Solution 4