Abusing JSON.stringify()
Abusing JSON.stringify
is very popular in the JavaScript world but many people fail to see the limitation and hidden dangers of using JSON.stringify()
for things that it was not made for. This blog post is actually about how to avoid this and use the right tools instead.
Use Case 1: Pretty-Print Data Structures
Front-end developers are often frustrated, when they find out that console.log()
on the server is, of course, not interactive but also only prints out the first three levels of deeply nested objects. More-deeply nested objects are only displayed anonymously as [Object]
or [Array]
.
One simple cure is to simply use console.log(JSON.stringify(obj, null, '\t'))
. The extra arguments null
and '\t'
(or a number for the indentation depth) cause JSON.stringify
to pretty-print the object.
Use Case 2: Deep-Cloning
But you can also create something like a deep clone with JSON.parse(JSON.stringify(obj))
. Not very elegant, not very efficient, but still very common.
Problems Using the JSON
Namespace Object
JSON was invented for exchanging data between applications written in arbitrary programming languages. Although it stands for JavaScript Object Notation, that does not mean that it can describe every detail of a JavaScript Object. A lot of stuff gets modified, when serialized into JSON, and some information is silently discarded.
Silent Modifications
Some things like cyclic references just don't work with JSON. That should actually be clear. But a lot of things are silently modified or even discarded.
Where are My Undefined Values?
I once had an application where the distinction between undefined
and null
was significant but from my debugging output I could see that wherever I expected an undefined
there was a null
. After some long and fruitless debugging sessions I finally realized that it was my own debugging code that did that. I was using console.log(JSON.stringify(obj, null, '\t'))
and that did, of course, coerce every undefined value into null
. That is nothing that you are proud of telling your colleagues.
NaN
and Infinity
They also get converted to null
.
One particularly nasty feature is that the sign of the infinity values will be removed:
const original = { big: +Infinity, medium: 0, small: -Infinity };
console.log(original.big > original.medium); // true
console.log(original.medium > original.small); // true
const clone = JSON.parse(JSON.stringify(original));
console.log(clone.big > clone.medium); // false
console.log(clone.medium > clone.small); // false
console.log(clone);
Again, this should not be a surprise to anybdoy. But it is very easy to forget, when your mind has taken the shortcut JSON.parse(JSON.stringify(obj))
equals deep cloning.
Functions
Try this here:
const obj = {
foo: () => {},
};
console.log(JSON.stringify(obj, null, '\t'));
The output is an empty object {}
. A function cannot be JSON encoded and it is therefore discarded. But what is maybe a little bit surprising is that not just the value but the entire property gets discarded. The key foo
no longer exists in the resulting object.
Non-Enumerable Properties and Symbol Properties
Take this example:
const obj = { [Symbol('key')]: 'value' };
Object.defineProperty(obj, 'hidden', { value: 'secret', enumerable: false });
console.log(JSON.stringify(obj, null, '\t'));
You may have guessed it already. The output is simply {}
.
Custom Serialization with toJSON()
Example:
let obj = {
value: 42,
toJSON() {
return { onlyValue: this.value };
}
};
console.log(JSON.stringify(obj)); // {"onlyValue":42}
The only time you notice the presence of a toJSON()
method is when serializing into JSON. And then it can be quite a surprise.
Prototypes are Ignored
Inherited properties from an object's prototype are not serialized, only the object's own properties are included.
function Person(name) {
this.name = name;
}
Person.prototype.age = 30;
const person = new Person('Alice');
console.log(JSON.stringify(person)); // {"name":"Alice"}
Phew!
Non-Numeric Array Indices
Recently, we had a deeper look into array indices in the post Hidden Quirks of JavaScript for...in
Loops. Do you remember that you can also assign named properties to arrays?
const arr = ['foo', 'bar', 'baz'];
arr['cowboys'] = ['Adam', 'Hoss', 'Little Joe'];
console.log(JSON.stringify(arr)); // ["foo","bar","baz"]
Where have all my cowboys gone?
Don't get me wrong. I am not trying to find flaws in the JSON definition. There is a good reason to treat all these cases in the way they are treated. Only, when you abuse JSON.stringify()
for other things than normalized data exchange, you will sooner or later forget about one of these hidden quirks, and that will be in the wrong moment.
Doing it the Right Way
Fortunately, you have better tools at hand for both visualizing deeply nested data and for deep cloning.
Visualizing Deeply Nested Data
If you want to visualize deeply nested data, for example for debugging, use the static method console.dir()
. It comes with an optional second argument options
that controls its behaviour. The options
argument can have the following optional properties:
color
: A boolean value: iftrue
, style the properties of the object according to their type. Defaults totrue
.depth
: A number representing the number of nesting levels to print when an object contains other objects or arrays. The valuenull
means: print all levels. Defaults to 2.showHidden
: A boolean value: iftrue
, print the object's non-enumerable and symbol properties. Defaults tofalse
.
So, wherever you used this up to now:
console.log(JSON.stringify(obj, null, 4));
Now use this instead:
console.dir(obj, { depth: null });
It is even shorter! In esoterical cases you have to add the option showHidden
to also show non-enumerable and symbol properties.
Deep-Cloning
Today, you should almost always be able to use structuredClone():
const copy = structuredClone(original);
The structuredClone()
method is nowadays widely available but in case it is not you can fall back to Lodash's cloneDeep() instead. Both are better than using JSON.parse(JSON.stringify(obj))
.
Conclusion
There is nothing wrong with JSON.stringify()
and there is also nothing wrong with using or abusing it. But when you use JSON.stringify()
for things that it was not intended for, you run the risk that you overlook or forget one of its hidden quirks. Avoid that by using console.dir()
or structuredClone()
instead.
Leave a comment