10

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?

treznik
  • 7,955
  • 13
  • 47
  • 59
  • 2
    That's a strange question, prototyped properties shouldn't be part of the stringified object, and any other property will be serializable, so there's generally no need to check this ? – adeneo Jun 01 '15 at 18:03
  • I'm with @adeneo on this one. JSON.stringify is meant to solely put data stored in JavaScript data structures into a string format to ease transfer of said data. Functions, such as prototypical properties, are not valid JSON; therefore, they would not be stringified. Why do you need to check if they're serializable? Are you not sure if you're being passed a POJO or an instance? – tsiege Jun 01 '15 at 18:16
  • 1
    @treznik What do you mean for serializable here? Json parse will stringify only the properties of the object. Are you looking for a way to serialize every thing? – Giuseppe Pes Jun 01 '15 at 19:56
  • My use case is a bit complex, see https://github.com/skidding/cosmos. Basically I have an online editor for JSON "fixtures". I want to let the user edit the serializable keys from that fixture object, yet still extend what the user composes with the unserializable keys from the initial fixture object. Hope it makes sense. Anyway, see my own accepted answer to see what I went for and let me know if you have further ideas. – treznik Jun 08 '15 at 15:12

5 Answers5

5
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.)

bardzusny
  • 3,788
  • 7
  • 30
  • 30
  • Thanks, I also created something similar in the meantime. So your comment is a confirmation that this is the way to go (at least for now). I also posted my solution since I find it simpler. Let me know if you detect any output differences, though. – treznik Jun 08 '15 at 15:08
  • Without lodash -- `typeof obj === 'null' || (obj.constructor === Object && obj.toString() === '[object Object]')` -- https://stackoverflow.com/questions/5876332/how-can-i-differentiate-between-an-object-literal-other-javascript-objects – Polv Nov 13 '19 at 10:59
  • This returns `false` for `null` (which is JSON serializable) and `true` for `undefined` (which is not). I was able to adapt it to my needs though. – Nate Jun 21 '21 at 19:23
4

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;
};
treznik
  • 7,955
  • 13
  • 47
  • 59
  • Yes, I would say your code is nicer and more consistent than mine (using lodash methods all around). I would just add that unless you're quite specific about what is "serializable" (running custom checks), simple `JSON.parse(JSON.stringify(obj))` would probably suffice. – bardzusny Jun 09 '15 at 00:19
  • Used this because my linter complained about `for...in` `if (_.find(obj, element => !isSerializable(element)) != null) { return false; }` – Saltymule Dec 01 '16 at 12:57
  • Bug / failure case: An Object key can be a Symbol() instead of a string, which is not serializable. This code will not detect that. – Michael G Nov 19 '22 at 00:08
1

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: () => {}}}]))
        });

    });

}
John Walker
  • 513
  • 4
  • 16
  • This has the same problem as the accepted answer. A plain object can have own-property symbols as keys. E.g., `var plainObject = { [Symbol()]: "I'm Not Serializable" }` – Michael G Nov 19 '22 at 00:16
0

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;
  }
};
Etienne Martin
  • 10,018
  • 3
  • 35
  • 47
0

Here's my solution with vanilla JS using pattern matching. It correctly flags Symbol() keys as non-serializable, a problem I ran into with the other code listed here.

It's also nicely concise, and maybe a bit more readable.
Returns true if the parameters can be serialized to JSON, returns false otherwise.

const isSerializable = n => (({

    [ !!"default" ]: () => false,
    [ typeof n === "boolean" ]: () => true,
    [ typeof n === "string" ]: () => true,
    [ typeof n === "number" ]: () => true,
    [ typeof n === "object" ]: () => 
        ! Object.getOwnPropertySymbols( n ).length &&
        isSerializable( Object.entries( n ) ),
    [ Array.isArray( n ) ]: () => ! n.some( n => ! isSerializable( n ) ),
    [ n === null ]: () => true,

})[ true ])();

Michael G
  • 458
  • 2
  • 9