How to stringify class instances in Javascript and Express.js

adamcoster

Adam Coster

Posted on September 2, 2020

How to stringify class instances in Javascript and Express.js

When trying to take an object-oriented approach in Javscript, you'll run into the following problem: The native JSON.stringify() method, which works wonders for plain objects, is basically useless for class instances.

For example:

const plainObject = {
  hello: 'world',
  something: 'else',
  nested: {item: {number:10}}
};
JSON.stringify(plainObject);
// Yields the perfect JSON string:
// '{"hello":"world","something":"else","nested":{"item":{"number":10}}}'

class MyFancyClass{
  #something;
  constructor(){
    this.hello = 'world';
    this.#something = 'else';
  }
  get item(){
    return {number:10};
  }
  get nested(){
    return {item: this.item}
  }
}
const instanceOfMyFancyClass = new MyFancyClass();
JSON.stringify(instanceOfMyFancyClass);
// Yields:
// '{"hello":"world"}'
Enter fullscreen mode Exit fullscreen mode

(That #something syntax is modern JavaScript's way of identifying private properties, which is why it doesn't show up in the JSON string.)

Using .toJSON()

When you call JavaScript's built-in JSON.stringify() function on an object, it looks for a toJSON() function on that object and, if such a function exists, stringifies the result of that function's return value.

This is super handy, since it allows you to add a toJSON() method to your class and output a plain object for stringifying.

class MyStringifiableClass {
  #something;
  constructor(){
    this.hello = 'world';
    this.#something = 'else';
  }
  get item(){
    return {number:10};
  }
  get nested(){
    return {item: this.item}
  }
  toJSON(){
    return {
      hello: this.hello,
      something: this.#something,
      nested: this.nested
    }
  }
}

const stringifiableInstance = new MyStringifiableClass();
JSON.stringify(stringifiableInstance);
// '{"hello":"world","something":"else","nested":{"item":{"number":10}}}'
Enter fullscreen mode Exit fullscreen mode

Using .toObject() and a JSON.stringify replacer

But what if you don't have control over the guts of the thing you want to stringify, or for other reasons can't use toJSON()? Or what if you need to be able to stringify the same object in different ways?

An approach I've seen in a few JavaScript libraries is to include a method .toObject() that returns a plain object representation of a class instance. Presumably the method name is mirroring the built-in .toString() method that all JavaScript objects have. In any event, having a .toObject() method on your classes is great for a lot of reasons, but one thing it does not help you with is JSON stringification.

That is, unless you take advantage of the "replacer" argument in JavaScript's native JSON.stringify method. By updating the replacer to attempt to use .toObject(), and then using that method in all your custom classes (and extending built-in classes to add it), you can still use JSON.stringify in a meaningful way on class instances.

class MyStringifiableClass {
  #something;
  constructor(){
    this.hello = 'world';
    this.#something = 'else';
  }
  get item(){
    return {number:10};
  }
  get nested(){
    return {item: this.item}
  }
  toObject(){
    return {
      hello: this.hello,
      something: this.#something,
      nested: this.nested
    }
  }
}

/**
 * @param {string} key
 * @param {any} value
 */
function jsonStringifyToObjectReplacer(key,value){
  if(value && value.toObject){
    return value.toObject();
  }
  return value;
}

const stringifiableInstance = new MyStringifiableClass();
JSON.stringify(stringifiableInstance,jsonStringifyToObjectReplacer);
// And now we're back to where we started:
// '{"hello":"world","something":"else","nested":{"item":{"number":10}}}'
Enter fullscreen mode Exit fullscreen mode

For convenience, wrap your own function around the native stringifier and use that throughout your application:

function stringify(something){
  return JSON.stringify(something,jsonStringifyToObjectReplacer);
}
Enter fullscreen mode Exit fullscreen mode

You could even combine the use of toJSON() and toObject (or other custom functions, along with the built-in JSON stringifier and custom ones with your own replacers, to output different stringified versions of your class instances depending on context!

Change the Express.js JSON.stringify replacer

The Express.js framework is stellar for rapidly setting up a RESTful server. One of its many great traits is how it handles sending data back to clients: if you attempt to send some sort of JavaScript object Express will automatically JSON stringify that object for you and set the Content-Type header to "application/json".

// Express server setup
// See {@link https://expressjs.com/en/starter/hello-world.html}
const express = require('express');
const app = express();
app.listen(8080);

app.get('/',(req,res)=>{
 res.send({hello:'world'});
 // Client recieves '{"hello":"world"}' body
 // with content-type header set to "application/json"
});
Enter fullscreen mode Exit fullscreen mode

But what if you want to send a class instance to the client? Express uses the regular JSON.stringify method on the data you send, so we're right back to the original problem. Fortunately, Express lets you set any replacer you want:

app.set('json replacer',jsonStringifyToObjectReplacer)
Enter fullscreen mode Exit fullscreen mode

And with that, you can simply res.send(myClassInstance) throughout your entire Express application, for any classes that include the .toObject() method.

💖 💪 🙅 🚩
adamcoster
Adam Coster

Posted on September 2, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related