In JavaScript, the JSON.stringify() function looks for functions named toJSON in the object being serialized. If an object has a toJSON function, JSON.stringify() calls toJSON() and serializes the return value from toJSON() instead.

For example, the below script prints the same thing as JSON.stringify({ answer: 42 }).

const json = JSON.stringify({
  answer: { toJSON: () => 42 }
});

console.log(json); // {"answer":42}

With ES6 Classes

The toJSON() function is useful for making sure ES6 classes get serialized correctly. For example, suppose you have a custom JavaScript error class.

class HTTPError extends Error {
  constructor(message, status) {
    super(message);
    this.status = status;
  }
}

By default, JavaScript isn't great with serializing errors. The below script prints {"status":404}, no error message or stack trace.

class HTTPError extends Error {
  constructor(message, status) {
    super(message);
    this.status = status;
  }
}

const e = new HTTPError('Fail', 404);
console.log(JSON.stringify(e)); // {"status":404}

However, if you add a toJSON() method to your HTTPError class, you can configure how JavaScript serializes instances of HTTPError.

class HTTPError extends Error {
  constructor(message, status) {
    super(message);
    this.status = status;
  }

  toJSON() {
    return { message: this.message, status: this.status };
  }
}

const e = new HTTPError('Fail', 404);
console.log(JSON.stringify(e)); // {"message":"Fail","status":404}

You can even get fancy and make toJSON() serialize the stack trace if the NODE_ENV is development.

class HTTPError extends Error {
  constructor(message, status) {
    super(message);
    this.status = status;
  }

  toJSON() {
    const ret = { message: this.message, status: this.status };
    if (process.env.NODE_ENV === 'development') {
      ret.stack = this.stack;
    }
    return ret;
  }
}

const e = new HTTPError('Fail', 404);
// {"message":"Fail","status":404,"stack":"Error: Fail\n    at ...
console.log(JSON.stringify(e));

The neat thing about toJSON() is that JavaScript handles recursion for you, so it will still correctly serialize deeply nested HTTPError instances and HTTPError instances in arrays.

class HTTPError extends Error {
  constructor(message, status) {
    super(message);
    this.status = status;
  }

  toJSON() { 
    return { message: this.message, status: this.status };
  }
}

const e = new HTTPError('Fail', 404);
// {"nested":{"message":"Fail","status":404},"arr":[{"message":"Fail","status":404}]}
console.log(JSON.stringify({
  nested: e,
  arr: [e]
}));

Many libraries and frameworks use JSON.stringify() under the hood. For example, Express' res.json() function and Axios POST requests convert objects to JSON using JSON.stringify(). So custom toJSON() functions work with those modules as well.

toJSON() in the Wild

Many Node.js libraries and frameworks use toJSON() to ensure JSON.stringify() can serialize complex objects into something meaningful. For example, Moment.js objects have a nice simple toJSON() function that looks like this:

    function toJSON () {
        // JSON.stringify(new Date(NaN)) === 'null'
        return this.isValid() ? this.toISOString() : 'null';
    }

You can try it yourself by running:

const moment = require('moment');
console.log(moment('2019-06-01').toJSON.toString());

Node.js buffers also have a toJSON() function.

const buf = Buffer.from('abc');
console.log(buf.toJSON.toString());

// Prints:
function toJSON() {
  if (this.length > 0) {
    const data = new Array(this.length);
    for (var i = 0; i < this.length; ++i)
      data[i] = this[i];
    return { type: 'Buffer', data };
  } else {
    return { type: 'Buffer', data: [] };
  }
}

Mongoose documents also have a toJSON() function that ensures the internal state of Mongoose documents doesn't end up in JSON.stringify() output.

Moving On

The toJSON() function is an important tool when building classes in JavaScript. It is how you control how JavaScript serializes your class into JSON. The toJSON() function can help you solve numerous problems, like making sure dates or Node.js buffers get serialized in the right format for your app. Give it a shot next time you write an ES6 class.

Found a typo or error? Open up a pull request! This post is available as markdown on Github
comments powered by Disqus