I'm pretty sure you have used the global JSON
object for a variety of things, like in fetch requests and to avoid the dreaded [object Object]
. I also bet you didn't know about the rest of the largely unknown features that JSON
can provide!
JSON
can do cool stuff like revive data, use a custom format to encode/decode data, hide certain properties in stringified data, and format your JSON! 🤯
Sound interesting? Let's dive into it!
1. Formatting
The default stringifier also minifies the JSON, which looks ugly
const user = {
name: 'John',
age: 30,
isAdmin: true,
friends: ['Bob', 'Jane'],
address: {
city: 'New York',
country: 'USA'
}
};
console.log(JSON.stringify(user));
//=> {"name":"John","age":30,"isAdmin":true,"friends":["Bob","Jane"],"address":{"city":"New York","country":"USA"}}
JSON.stringify
has a built in formatter too!
console.log(JSON.stringify(user, null, 2));
// {
// "name": "John",
// "age": 30,
// "isAdmin": true,
// "friends": [
// "Bob",
// "Jane"
// ],
// "address": {
// "city": "New York",
// "country": "USA"
// }
// }
(If you are wondering what that null is, we'll come to it later)
In this example, the JSON was formatted with 2 spaces of indentation.
We can also specify a custom character to use for indentation.
console.log(JSON.stringify(user, null, 'lol'));
// {
// lol"name": "John",
// lol"age": 30,
// lol"isAdmin": true,
// lol"friends": [
// lollol"Bob",
// lollol"Jane"
// lol],
// lol"address": {
// lollol"city": "New York",
// lollol"country": "USA"
// lol}
// }
2. Hiding certain properties in stringified data
JSON.stringify
had a second argument which is largely unknown. It's called the replacer
and it's a function or array that decides which data to keep in the output and which not to.
Here's a simple example where we can hide the password
of a user.
const user = {
name: 'John',
password: '12345',
age: 30
};
console.log(JSON.stringify(user, (key, value) => {
if (key === 'password') {
return;
}
return value;
}));
And this is the output:
{"name":"John","age":30}
We can further refactor this:
function stripKeys(...keys) {
return (key, value) => {
if (keys.includes(key)) {
return;
}
return value;
};
}
const user = {
name: 'John',
password: '12345',
age: 30,
gender: 'male'
};
console.log(JSON.stringify(user, stripKeys('password', 'gender')))
Which outputs:
{"name":"John","age":30}
You can also pass an array to get certain keys only:
const user = {
name: 'John',
password: '12345',
age: 30
}
console.log(JSON.stringify(user, ['name', 'age']))
Which output the same thing.
The cool thing is this works on arrays too. If you had a huge array of cakes:
const cakes = [
{
name: 'Chocolate Cake',
recipe: [
'Mix flour, sugar, cocoa powder, baking powder, eggs, vanilla, and butter',
'Mix in milk',
'Bake at 350 degrees for 1 hour',
// ...
],
ingredients: ['flour', 'sugar', 'cocoa powder', 'baking powder', 'eggs', 'vanilla', 'butter']
},
// tons of these
];
We can easily do the same thing, and the replacer will be applied to each cake:
const cakes = [
{
name: 'Chocolate Cake',
recipe: [
'Mix flour, sugar, cocoa powder, baking powder, eggs, vanilla, and butter',
'Mix in milk',
'Bake at 350 degrees for 1 hour',
// ...
],
ingredients: ['flour', 'sugar', 'cocoa powder', 'baking powder', 'eggs', 'vanilla', 'butter']
},
// tons of these
];
console.log(JSON.stringify(cakes, ['name']))
We get this:
[{"name":"Chocolate Cake"},{"name":"Vanilla Cake"},...]
Cool stuff!
3. Using toJSON to create custom output formats
If an object implements the toJSON
function, JSON.stringify
will use it to stringify the data.
Consider this:
class Fraction {
constructor(n, d) {
this.numerator = n;
this.denominator = d;
}
}
console.log(JSON.stringify(new Fraction(1, 2)))
This would output {"numerator":1,"denominator":2}
. But what if we wanted to replace this with a string 1/2
?
Enter toJSON
class Fraction {
constructor(n, d) {
this.numerator = n;
this.denominator = d;
}
toJSON() {
return `${this.numerator}/${this.denominator}`
}
}
console.log(JSON.stringify(new Fraction(1, 2)))
JSON.stringify
respects the toJSON
property and output "1/2"
.
4. Reviving data
Our fraction example above works nicely. But what if we want to revive the data? Wouldn't it be cool if the fraction would be magically brought back when we parse the JSON again? We can!
Enter revivers!
class Fraction {
constructor(n, d) {
this.numerator = n;
this.denominator = d;
}
toJSON() {
return `${this.numerator}/${this.denominator}`
}
static fromJSON(key, value) {
if (typeof value === 'string') {
const parts = value.split('/').map(Number);
if (parts.length === 2) return new Fraction(parts);
}
return value;
}
}
const fraction = new Fraction(1, 2);
const stringified = JSON.stringify(fraction);
console.log(stringified);
// "1/2"
const revived = JSON.parse(stringified, Fraction.fromJSON);
console.log(revived);
// Fraction { numerator: 1, denominator: 2 }
We can pass a second argument to JSON.parse
to specify a reviver function. The job of the reviver is to "revive" stringified data back into it's original form. Here, we are passing a reviver, which is the static proprty fromJSON
of the Fraction
class.
In this case the reviver checks if the value is a valid fraction and if it is, it creates a new Fraction
object and returns it.
Fun fact: this feature is used in the built-in Date object. Try looking up
Date.prototype.toJSON
That's why this works:console.log(JSON.stringify(new Date())) //=> '"2022-03-01T06:28:41.308Z"'
To revive the date, we can use
JSON.parse
:function reviveDate(key, value) { const regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,}|)Z$/; if (typeof value === "string" && regex.test(value)) { return new Date(value); } return value; } console.log(JSON.parse('"2022-03-01T06:28:41.308Z"', reviveDate)) //=> Tue Mar 01 2022 06:28:41 GMT-0700 (Pacific Daylight Time)
5. Using revivers to hide data
Like resolvers, revivers can also be used to hide data. It works in the same way.
Here's an example:
const user = JSON.stringify({
name: 'John',
password: '12345',
age: 30
});
console.log(JSON.parse(user, (key, value) => {
if (key === 'password') {
return;
}
return value;
}));
And this is the output:
{ name: 'John', age: 30 }
As an exercise, check if you can rewrite the previously shown resolvers as revivers.
That's a wrap!
Let me know if you know any other cool JSON
tricks 👀
Thanks for reading!