JSON2.js vs Prototype

by Andrew Betts,

This post was originally made on the Assanka blog. Assanka was acquired by the Financial Times in January 2012, and became what is now FT Labs. Learn more.

We use Douglas Crockford‘s json2.js frequently in our web apps. Its stringify method allows JavaScript data structures to be trivially serialised before submission via AJAX to a web service. It works by descending through the structure, calling the toJSON() method on anything it finds. It also creates toJSON methods for data types that do not already have them, on the basis that future browsers will introduce toJSON() support – at which point the native implementation can be used because it’s likely to be a lot faster.

Recently I needed to use this method to serialise some data in a JavaScript library that might be used in ‘foreign’ web pages. My own library was nicely encapsulated, and didn’t interfere with any other JavaScript that might be running on the page, and it included Douglas Crockford’s JSON2.js implementation.

But on one of our clients’ sites, it didn’t work. I got this:

{"key":"val",[{"key":"val"},{"key":"val"}]}

What’s happened here is that any arrays in my data structure have been stringified twice. This didn’t happen in my dev environment. I narrowed down the differences and realised what was causing this effect. They’re using Prototype. We’re not.

Prototype modifies a number of JavaScript’s native objects, including the Array object, and… you guessed it, adds a toJSON() method to it. Unfortunately it does not return what Crockford’s JSON implementation is expecting. From the docs for json2:

A toJSON method does not serialize: it returns the value represented by the name/value pair that should be serialized, or undefined if nothing should be serialized.

Prototype’s toJSON() is serialising. There don’t seem to be any sensible solutions to this online, but it’s actually relatively simple to solve using a replacer function, allowed for in the json2 API:

var reqdata = JSON.stringify(req, function(key, value) {
	if (typeof this[key] == 'object' && Object.prototype.toString.apply(this[key]) === '[object Array]') {
		return this[key];
	} else {
		return value;
	}
});

Essentially this says ‘for each key in the data structure, if the value is an array, use the raw value, otherwise use the value you gave me’. This makes sense when you look at the sequence of steps that stringify() goes through for each key it encounters:

  1. If the value has a toJSON() method, call it.
  2. If a replacer function has been given, call it.
  3. If the remaining value is a scalar, return it.
  4. If the remaining value is an object, stringify each member, then concatenate keys and values within braces {key:val,key:val}
  5. If the remaining value is an array, stringify each element, then concatenate values within brackets [val,val,val]

So, stringify() has already called Prototype’s toJSON() method by the time it executes the replacer function, but we can use the replacer function to restore the original value, allowing stringify() to then deal with the array by calling itself recursively.

The result is that we can ensure that even if a toJSON() method does exist on the Array object, its output is ignored, and we then get the JSON string that we wanted.