JSON2.js vs Prototype
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:
- If the value has a toJSON() method, call it.
- If a replacer function has been given, call it.
- If the remaining value is a scalar, return it.
- If the remaining value is an object, stringify each member, then concatenate keys and values within braces {key:val,key:val}
- 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.
2 Comments
Thanks for these notes; it helped me figure out where Prototype was going wrong. That said, this replacer fn seems to only work for shallow objects — if I have an array with an object that itself contains an array, it will be stringified. I’m working on fixing that problem and if I come up with a solution that is any good I will post it here.
Update:
It ended up being easier to let Prototype do its own thing — I ended up looking for Prototype in the window and toJSON on the object and calling that if it exists. Additionally, in some browsers you may not be able to depend on the JSON.stringify implementation being exactly the same as the one in json2.js. I posted about this issue on my blog: http://agaskar.com/post/349461220/json-stringify-bug-with-native-ff-implementation