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 Set
s 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 — Set
s, WeakSet
s, Map
s and WeakMap
s are ignored or replaced by null
. There is hope though — the optional second argument of JSON.stringify()
allows us to escape all Set
s 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 💋
And using \t for the third argument makes it nicer when displayed.
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 👋
Didn't know about the number. :)
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
How would `'\t'` make it nicer when displayed? I always use `2` as an argument for the indentation.
What does `'\t'` do?
It is escaped tab.
But it increases the indentation more than 2. Try that
Yes, it increases spacing by the size of a `tab` :)
element.innerHTML = `<pre><code>${JSON.stringify(obj, null, 2)}</code></pre>`
is my best friend for debugging.You could also define a `toJSON` method on the respective object:
https://developer.mozilla.o...
dude.toString = function() {
return {
name: this.name,
enemies: null
};
};
Another little-known cool feature. Thanks!
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?
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.
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.
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"]}
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
);