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
?
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
?
- 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 Commented Jun 1, 2015 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 Commented Jun 1, 2015 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 Commented Jun 1, 2015 at 19:56
- My use case is a bit complex, see 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 Commented Jun 8, 2015 at 15:12
5 Answers
Reset to default 6function 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.)
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;
};
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: () => {}}}]))
});
});
}
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 ])();
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;
}
};