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"}'
(That #something
syntax is modern JavaScript's way of identifying private properties, which is why it doesn't show up in the JSON string.)
.toJSON()
Using 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}}}'
.toObject()
and a JSON.stringify replacer
Using 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}}}'
For convenience, wrap your own function around the native stringifier and use that throughout your application:
function stringify(something) {
return JSON.stringify(something, jsonStringifyToObjectReplacer);
}
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!
JSON.stringify
replacer
Change the Express.js 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"
});
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);
And with that, you can simply res.send(myClassInstance)
throughout your entire Express application, for any classes that include the .toObject()
method.