Skip to content

TIL — The power of JSON.stringify replacer parameter

I had an interesting problem that melted my brain a bit. Let me share what I have learned and maybe save you a few moments when you come across a similar challenge. Lets have some fun with JSON.stringify().

const dude = {
  name: "Pawel",
  friends: ["Dan", "Pedro", "Mr Gregory"]
};
const dudeStringified = JSON.stringify(dude);

console.log(dudeStringified);
// {"name":"Pawel","friends":["Dan","Pedro","Mr Gregory"]}

No surprises here. Unfortunately, the architecture used on my project (AWS DynamoDB for curious beasts) forced me to deal with ECMAScript Sets and things became more interesting. Just look at this.

const dude = {
  name: "Pawel",
  friends: new Set(["Dan", "Pedro", "Mr Gregory"])
};
const dudeStringified = JSON.stringify(dude);

console.log(dudeStringified);
// {"name":"Pawel","friends":{}}

I assumed that a set of values is going to be converted to a good old plain array. As you may have guessed I was wrong — Sets, WeakSets, Maps and WeakMaps are ignored or replaced by null. There is hope though — the optional second argument of JSON.stringify() allows us to escape all Sets and convert them to an array.

const dude = {
  name: "Pawel",
  friends: new Set(["Dan", "Pedro", "Mr Gregory"])
};
const dudeStringified = JSON.stringify(dude, (key, value) =>
  value instanceof Set ? [...value] : value
);

console.log(dudeStringified);
// {"name":"Pawel","friends":["Dan","Pedro","Mr Gregory"]}

Problem solved 👏

(TIL) Today I learned #

JSON.stringify() takes a second optional argument that can be a recursive replacer function or an array of white-listed keys to be stringified. Like so…

// Second argument as a replacer function

const dude = {
  name: "Dan"
};
const dudeStringified = JSON.stringify(dude, (key, value) =>
  key === "name" ? "Pawel" : value
);

console.log(dudeStringified);
// {"name":"Pawel"}
// Second argument as an array of white-listed keywords

const dude = {
  name: "Pawel",
  friends: new Set(["Dan", "Pedro", "Mr Gregory"])
};

const dudeStringified = JSON.stringify(dude, ["name"]);

console.log(dudeStringified);
// {"name":"Pawel"}

Third argument can be a string or a number. It decides about the number of spaces or text to used as a delimiter. Look!

// Third argument as a number

const dude = {
  name: "Pawel",
  friends: ["Dan", "Pedro", "Mr Gregory"]
};
const dudeStringified = JSON.stringify(dude, null, 4);

console.log(dudeStringified);
// {
//   "name": "Pawel",
//   "friends": [
//       "Dan",
//       "Pedro",
//       "Mr Gregory"
//   ]
// }
// Third argument as a string

const dude = {
  name: "Pawel",
  friends: ["Dan", "Pedro", "Mr Gregory"]
};
const dudeStringified = JSON.stringify(dude, null, "🍆");

console.log(dudeStringified);
// {
// 🍆"name": "Pawel",
// 🍆"friends": [
// 🍆🍆"Dan",
// 🍆🍆"Pedro",
// 🍆🍆"Mr Gregory"
// 🍆]
// }

Until next time, stay curious 💋

Comments

  • R
    Raymond Camden

    And using \t for the third argument makes it nicer when displayed.

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      Yes, I use it all the time.

      Did you know that third argument can be a string (like you suggested) or a number? If it is a number it a number of spaces, if string a text that will be used as a delimiter.

      Thanks for your input. Have a great weekend @cfjedimaster:disqus 👋

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
      • R
        Raymond Camden

        Didn't know about the number. :)

        👆 you can use Markdown here

        Your comment is awaiting moderation. Thanks!
      • C
        Colin Richardson

        I only found that out when I switched to Typescript and it came up as an option. I thought it would put the number instead of a space, thought it was very odd, til I tried it and realised it was the number of spaces. So now it's always stringify(value, null, 2) for me.
        Shame about always having to add the null

        👆 you can use Markdown here

        Your comment is awaiting moderation. Thanks!
    • C
      Christophe Nabil Nicolas El-Kh

      How would `'\t'` make it nicer when displayed? I always use `2` as an argument for the indentation.
      What does `'\t'` do?

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
      • Pawel Grzybek
        Pawel Grzybek

        It is escaped tab.

        👆 you can use Markdown here

        Your comment is awaiting moderation. Thanks!
        • C
          Christophe Nabil Nicolas El-Kh

          But it increases the indentation more than 2. Try that

          👆 you can use Markdown here

          Your comment is awaiting moderation. Thanks!
          • Pawel Grzybek
            Pawel Grzybek

            Yes, it increases spacing by the size of a `tab` :)

            👆 you can use Markdown here

            Your comment is awaiting moderation. Thanks!
  • K
    Kevin Baugh

    element.innerHTML = `<pre><code>${JSON.stringify(obj, null, 2)}</code></pre>` is my best friend for debugging.

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
  • d
    disqus_zZ9mWbJZCD

    You could also define a `toJSON` method on the respective object:

    If the value has a toJSON() method, it's responsible to define what data will be serialized.

    https://developer.mozilla.o...


    dude.toString = function() {
    return {
    name: this.name,
    enemies: null
    };
    };

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      Another little-known cool feature. Thanks!

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
    • A
      Andy Farrell

      Given that, would it be reasonable to add a .toJSON() method to Set.prototype which returned the Set as an array, thus neatly (maybe?) working around the issue?

      Or does .stringify() only look for a .toJSON() method on the its first argument?

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
      • d
        disqus_zZ9mWbJZCD

        Generally speaking, you wanna avoid extending built-ins, so I'd advise sticking with the replacer function for that.

        But yeah, `JSON.stringify` does invoke `toJSON` also for nested objects.

        👆 you can use Markdown here

        Your comment is awaiting moderation. Thanks!
  • f
    fetishlace

    Good to know about Map and Set behavior with JSON.stringify() and good is every article about 2 other arguments since very few people knowing about it and using it :-)
    I am using mainly 3rd argument for some months and mainly for printing arrays for debug or some export, since it works for arrays the same way it works for objects.

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
  • B
    Bruno Vieira

    Did you try using Array.from()? like below... it works to me



    const dude = {
    name: "Pawel",
    friends: Array.from(new Set(["Dan", "Pedro", "Mr Gregory"]))
    };

    const str = JSON.stringify(dude);

    console.log(str);
    //{"name":"Pawel","friends":["Dan","Pedro","Mr Gregory"]}

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • f
      fetishlace

      Sure, JSON.stringify has no problems with arrays and objects but problem is with Maps and Sets returning "{}" in output.
      That replacement function used with JSON.stringify is doing the same spreading set into array. [...someSet] and Array.from(someSet) are doing the same thing.

      const dudeStringified = JSON.stringify(dude, (key, value) =>
      value instanceof Set ? [...value] : value
      );

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!

What'ya think?

👆 you can use Markdown here

Your comment is awaiting moderation. Thanks!