evantian.me

You don't know JSON.stringify / JSON.parse

June 2, 2019 • ☕️ 4 min read

JSON.stringify is absolutely one of my favorite JavaScript’s native methods. It’s really helpful when you want to do a deep copy with some objects. Of course, there are some “Gocha” moments during this. So let’s take a look.

Getting Started

Before going deep into it, let’s take a overview first.

The JSON.stringify() method converts a JavaScript object or value to a JSON string, optionally replacing values if a replacer function is specified or optionally including only the specified properties if a replacer array is specified.

“converts a JavaScript object or value to a JSON string”, hmmm, sounds normal.

“replacing values if a replacer function is specified”, ok, cool.

“including only the specified properties if a replacer array is specified”, wait, what?

I believe some of my readers might have the same feeling above when first checking out the definition of JSON.stringify. Most of us got the impression that the only job it can do is a deep clone(with the help of JSON.parse of course). Like this below:

const obj1 = { name: 'Ross' }

const obj2 = JSON.parse(JSON.stringify(obj1)) // { name: 'Ross' }

Actually it can do a better job than this, even more! Let’s check out the siganature first:

JSON.stringify(value[, replacer[, space]])

See, JSON.stringify can accept 3 parameters in fact: value for the thing you want to convert, replacer for the function or array if you need to do some filter or replace job when converting, space for the separate marker.

value

Let’s talk it straight, what kind of values are non-stringifiable?

circular structure, Function, undefined, Symbol, Infinity, NaN.

For circular structure, JavaScript will throw an error when you trying to convert it using JSON.stringify:

const foo = {}
foo.content = fooJSON.stringify(foo)
// Uncaught TypeError: Converting circular structure to JSON

// Same as array
const bar = []
bar[0] = barJSON.stringify(bar)
// Uncaught TypeError: Converting circular structure to JSON

A Best practice is that you need to wrap JSON.stringify with try/catch because the circular structure could happen anywhere in production.

For Function, undefined and Symbol, Object just drop them and Array replace them with null; For Infinity and NaN, they will all be treated as null:

const foo = {
  a: function() {},
  b: undefined,
  c: Symbol(),
  d: Infinity,
  e: NaN
}
JSON.stringify(foo)  // "{"d":null,"e":null}"

const bar = [ function(){}, undefined, Symbol(), Infinity, NaN ]
JSON.stringify(bar) // "[null, null, null, null, null]"

Btw, I didn’t figure out why the special values get dropped in Object and get replaced with null in Array. I mean to keep the shape(no strip out) is always a good choice, and it can help us know which value is transformed.

replacer

Although JSON.stringify deal with some special value in a “bad” way, we could format the output in a human-readable way by replacer :

const foo = {
  a: Symbol('foo'),
  b: 1
}

JSON.stringify(foo, (_, v) => typeof v === 'symbol' ? 'A symbol' : v)
// "{"a":"A symbol","b":1}"

Here’s the definition:

A function that alters the behavior of the stringification process, or an array of String and Number objects that serve as a whitelist for selecting/filtering the properties of the value object to be included in the JSON string.

Pretty clear, right?

By replacer, we can deal with Functions or undefined etc as the same way.

Here is another use case for replacer as a whitelist:

const foo = {
  name: 'Chandler',
  age: 29,
  sex: 'male',
  job: 'satistical analysis and data reconfiguration'  // Yeah, I know it 🤷‍♂
}

const whiteList = [ 'name', 'job' ]

JSON.stringify(foo, whiteList)
// "{"name":"Chandler","job":"satistical analysis and data reconfiguration"}"

space

space is used to control space of the output. We can give it a string or a number.

If it is a number, successive levels in the stringification will each be indented by this many space characters (up to 10). If it is a string, successive levels will be indented by this string (or the first ten characters of it).

toJSON()

In fact, we can customize JSON stringification behavior by giving a property named toJSON to an object as below(copied from MDN):

var obj = {
  data: 'data',
  
  toJSON(key){
    if(key)
      return `Now I am a nested object under key '${key}'`;
    else
      return this;
  }
};

JSON.stringify(obj);
// '{"data":"data"}'

JSON.stringify({ obj })
// '{"obj":"Now I am a nested object under key 'obj'"}'

JSON.stringify([ obj ])
// '["Now I am a nested object under key '0'"]'

To be continued…