5 Secret features of JSON you didn't know about 🤯

5 Secret features of JSON you didn't know about 🤯

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!

Â