Unleashing the power of toJSON method in JavaScript

The toJSON method is used to override the default serialization behavior of an Object, Array or Function in Javascript, serialized using the JSON.stringify static method. Suppose you have a user object or class in your Node.js backend project, you will want to remove the password property when sending this object to the client.

In some cases, you might want to add a new property to an object during serialization or send a custom Error object with a message property to the client, all these can be done automatically by adding a toJSON method to the object.

Prerequisite

We recommend having at least a basic knowledge of Javascript to follow along.

Major usages of the toJSON method

When JSON.stringify is called on a Javascript object or ES6 Class instance;

  1. it looks for a method called toJSON inside this object or class instance.

  2. If the object or class instance contains the toJSON method, JSON.stringify calls it.

  3. When toJSON method returns an object.

  4. JSON.stringify performs the above process recursively for properties in the returned object whose values in the current object or class instance are objects.

  5. All the return values are then serialized.

Adding new property/properties to serialized Javascript objects or class instances:

For Javascript Objects:

const user = {
    firstName: 'John',
    lastName: 'Doe',
    email: 'johndoe@gmail.com',
    password: 'password',
    toJSON() {
        return {
            email: this.email,
            fullName: this.firstName + ' ' + this.lastName
        }
    }
}

console.log(JSON.stringify(user));
//{"email":"johndoe@gmail.com","fullName":"John Doe"}

For Javascript ES6 Class Instance:

class User {
    constructor(firstName, lastName, email, password) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
        this.password = password;
    }

    toJSON() {
        const {email, firstName, lastName} = this;
        return {
            email,
            fullName: firstName + ' ' + lastName
        }
    }
}
console.log(JSON.stringify(new User('John', 'Doe', 'johndoe@gmail.com', 'password')));
// {"email":"johndoe@gmail.com","fullName":"John Doe"}

Removing existing property/properties from objects or class instances during serialization.

For Javascript Objects:

const book = {
    title: 'User guide',
    author: 'Herry Lam',
    published_year: 1992,
    toJSON() {
        delete this.published_year;
        return this;
    }
}

console.log(JSON.stringify(book));
// {"title":"User guide","author":"Herry Lam"}

For Javascript ES6 Class Instance:

class User {
    constructor(name, email, password) {
        this.name = name;
        this.email = email;
        this.password = password;
        this.order = null;
    }
    toJSON() {
        delete this.password;
        return this;
    }
}
console.log(JSON.stringify(new User('Mary Doe', 'marydoe@gmail.com', 'password')));
// {"name":"Mary Doe","email":"marydoe@gmail.com"}

Practical usage with Mongoose Schema:

import {Schema} from 'mongoose';

const userSchema = new Schema({
    email: {
        required: true,
        type: String
    },
    name: {
        type: String,
        required: true
    },
    password: {
        type: String
    }
}, {
    timestamps: true
})


userSchema.method('toJSON', function (){
    const userObject = this.toObject();
    delete userObject.password;
    return userObject;
})

More complex usage involving serializing nested objects and arrays

The code below models a user with an order containing some products. The user's password and the product's rating will be removed when an instance of the User class is serialized.

class User {
    constructor(name, email, password) {
        this.name = name;
        this.email = email;
        this.password = password;
        this.order = null;
    }

    toJSON() {
        delete this.password;
        return this;
    }
}

class Order {
    constructor(name) {
        this.name = name;
        this.products = [];
        this.total = 0;
    }

    addProducts(products) {
        this.products = this.products.concat(products);
        let total = 0;
        if(Array.isArray(products)){
            total = products.reduce((acc, product) => acc + product.price, 0);

        }else {
            total = products.price;
        }
        this.total += total;
    }

}


class Product {
    constructor(name, price, rating) {
        this.name = name;
        this.price = price;
        this.rating = rating || 0;
    }

    toJSON() {
        delete this.rating;
        return this;
    }
}


const order = new Order('Order 1');
const firstProduct = new Product('Shoe', 20, 2);
const secondProduct = new Product('Shirt', 50, 3);

order.addProduct([firstProduct, secondProduct]);


const user = new User('John Doe', 'johndoe@gmail.com', 'password');
user.order = order;
console.log(JSON.stringify(user));
// {"name":"John Doe","email":"johndoe@gmail.com","order":{"name":"Order 1","products":[{"name":"Shoe","price":20},{"name":"Shirt","price":50}],"total":70}}

Serializing Custom Errors in Javascript

By default, Javascript doesn't serialize custom Errors properly. The code below prints {"code": 500} with no error message or stack trace.

class RequestError extends Error {
    constructor(message, code = 500) {
        super(message);
        this.code = code;
    }
}
console.log(JSON.stringify(new RequestError('An error occured')));
// {"code": 500}

We can override this default serialization behavior by adding a toJSON method.

class RequestError extends Error {
    constructor(message, code = 500) {
        super(message);
        this.code = code;
    }

    toJSON() {
        const {message, code} = this;
        return {
            message,
            code
        }
    }
}

console.log(JSON.stringify(new RequestError('An error occured')));
// {"message":"An error occurred","code":500}

Conclusion

The toJSON method as seen above can be used to customize the serialization of objects and class instances in Javascript, JSON.stringify also calls the toJSON method recursively inside child objects or class instances which makes it very powerful.