Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Lisa Tierney
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Lisa Tierney

Some Reflections On How Express.js Prepares JSON Responses

By on

The other day, I was talking to fellow InVisioneer, Casey Flynn, about serializing response data in an Express.js application using Node.js. He mentioned that Express will call .toJSON() on the object being returned; and, that this might provide a way for me to remove some of the object's "internal use only" properties. After looking through the Express.js documentation and finding no mention of this, I realized that this wasn't an Express.js feature. Rather, it was a native JavaScript / Node.js feature that was being implicitly consumed as part of the way Express.js prepares JSON (JavaScript Object Notation) responses. Since this confused me at first, I thought it might be worth taking a quick look at how Express.js manages JSON.

As per the Express.js documentation, if you call response.json() - or you call response.send() and pass it an object - Express.js will use the native JSON.stringify() method to serialize the given value. This means that the JSON serialization algorithm in Express.js will inherently use available .toJSON() methods. And, if we set an application-wide "json replacer" config value, Express.js will also use a custom replacer function.

To demonstrate this, I've setup a small demo with a single route that returns a JSON payload. The object being serialized will provide its own .toJSON() method. And, at the application config level, we'll provide a custom replacer function:

// Require the core node modules.
var chalk = require( "chalk" );
var express = require( "express" );

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

var app = express();

// Under the hood, when we send back a JSON response, Express.js is just calling the
// native JSON.stringify() method. By default, no "replacer" method is used; but, we
// can set a replacer method as a GLOBAL APPLICATION SETTING (ie, this is not request
// specific, the same replacer is applied to every JSON response).
app.set(
	"json replacer",
	function replacer( key, value ) {

		console.log( chalk.red( "Calling JSON.stringify() with Replacer ( %s )." ), value );

		// Slightly transform data for demo purposes.
		if ( typeof( value ) === "string" ) {

			return( value + " (Replaced)" );

		}

		return( value );

	}
);

// Setup the demo route that returns a JSON payload.
app.get(
	"/",
	function( request, response, next ) {

		console.log( chalk.green.bold( "Routing:", request.url ) );

		// Again, under the hood, when we send back a JSON response, Express.js is
		// just calling the native JSON.stringify() method. As part of the normal JSON
		// feature-set, Node.js will recursively check to see if the given object graph
		// contains a .toJSON() method, and if available, use it to prepare the response.
		response.json({
			hello: "world",
			foo: "bar",

			// Custom toJSON() method for stringification purposes. Notice that we are
			// not invoking the method here - we are just supplying it as part of the
			// object graph being sent to the client.
			toJSON: function() {

				console.log( chalk.red( "Calling .toJSON()." ) );

				// Slightly transform data for demo purposes.
				return({
					hello: this.hello.toUpperCase(),
					foo: this.foo.toUpperCase()
				});

			}
		});

	}
)

app.listen( "8080" );

Now, when we spin up this Express.js application and make a request, we get the following terminal output:

Express.js uses JSON.stringify() under the hood, which exposes some surprising behaviors.

As you can see, the underlying JSON.stringify() call did two things. First, it invoked the .toJSON() method on the response object in order to get the transformed object. Then, it took the transformed object and recursively applied the global "replacer" function in order to prepare the sub-nodes of the object graph. And, if we look at what the client received, we can see that both sets of operations are present:

{"hello":"WORLD (Replaced)","foo":"BAR (Replaced)"}

So, we clearly see that Express.js is able to leverage the native behavior of the JSON.stringify() method. But, something about this feels funky. I don't think I'd want to consume these features as part of this implicit behavior. For one thing, it tightly couples the expected behavior of the response to the internal implementation of Express.js. Yes, the documentation states that it uses JSON.stingify(). But, what happens if the framework moves to a different serialization algorithm? Now, all of this implicit behavior goes out the window.

Of course, if Express.js moved away from JSON.stringify(), it would be a breaking change, so you would expect behavior to break. But, that's not even my biggest objection. First and foremost, I don't like how hidden this feature is. If I'm a developer and I look at this line of code:

response**.send**( modelInstance );

... there's absolutely nothing here that indicates modifications to the serialization process. If the modelInstance has a .toJSON() method or the application has a custom "replacer" config, the result will be surprising. And, I don't want code to be surprising; I want code to be obvious.

Rather than implicitly using JSON's serialization hooks, I'd prefer to see explicit method calls:

response.send( modelInstance.toDataTransferObject() );

Or, perhaps something like:

response.send( serializeModel( modelInstance ) );

Now, if I'm a developer reading this code, it becomes obvious as to where I need to look if I want to understand how the data will be prepared from the response.

Express.js makes it really easy to return JSON responses to the client. But, by using JSON.stringify() under the hood, Express opens the door to some surprising JSON transformation hooks. And, while these hooks are powerful, they are not self-evident. I'd prefer to see non-default serialization handlers moved into more explicit portions of the code where they become obvious to the developers that are maintaining the application.

Epilogue

To be clear, I am not saying JSON.stringify() is bad; or that the .toJSON() and replacer features are bad. I am just saying that they should be used explicitly in the application rather then implicitly by an implementation detail of the Express.js framework internals.

Want to use code from this post? Check out the license.

Reader Comments

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel