Abusing JSON.stringify()

Abusing JSON.stringify is very popular in the JavaScript world but many people fail to see the limitations 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.

Carpentry Tools
Photo by Étienne Girardet on Unsplash

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 the 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: if true, style the properties of the object according to their type. Defaults to true.

  • depth: A number representing the number of nesting levels to print when an object contains other objects or arrays. The value null means: print all levels. Defaults to 2.

  • showHidden: A boolean value: if true, print the object's non-enumerable and symbol properties. Defaults to false.

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

Abusing JSON.stringify()

Hidden Quirks of JavaScript `for...in` Loops

Creating E-Invoices with Free and Open Source Software

Dynamic Angular Configuration

Compiling ImageMagick for Perl

Standalone Angular Tour Of Heroes

This website uses cookies and similar technologies to provide certain features, enhance the user experience and deliver content that is relevant to your interests. Depending on their purpose, analysis and marketing cookies may be used in addition to technically necessary cookies. By clicking on "Agree and continue", you declare your consent to the use of the aforementioned cookies. Here you can make detailed settings or revoke your consent (in part if necessary) with effect for the future. For further information, please refer to our Privacy Policy.