5 Secret features of JSON in JavaScript you didn't know about 🤯
Tags: javascript, webdev, tutorial, tips
Published: 2022-03-01 10:24:19

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
```javascript
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!

```javascript
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.

```javascript
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.

```javascript
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:

```json
{"name":"John","age":30}
```

We can further refactor this:

```javascript
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:

```json
{"name":"John","age":30}
```

You can also pass an array to get certain keys only:

```javascript
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:

```javascript
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:

```javascript
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:

```json
[{"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:

```javascript
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`

```javascript
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!

```javascript
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:
> ```javascript
> console.log(JSON.stringify(new Date()))
> //=> '"2022-03-01T06:28:41.308Z"'
> ```
>
> To revive the date, we can use `JSON.parse`:
> ```javascript
> 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:

```javascript
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:

```javascript
{ 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!