90

I'm trying to figure out what's gone wrong with my json serializing, have the current version of my app with and old one and am finding some surprising differences in the way JSON.stringify() works (Using the JSON library from json.org).

In the old version of my app:

 JSON.stringify({"a":[1,2]})

gives me this;

"{\"a\":[1,2]}"

in the new version,

 JSON.stringify({"a":[1,2]})

gives me this;

"{\"a\":\"[1, 2]\"}"

any idea what could have changed to make the same library put quotes around the array brackets in the new version?

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
morgancodes
  • 25,055
  • 38
  • 135
  • 187
  • 4
    looks like it's a conflict with the Prototype library, which we introduced in the newer version. Any ideas how to stringify a json object containing an array under Prototype? – morgancodes Apr 02 '09 at 17:03
  • 27
    that's why people should refrain mangling with global built-in objects (as prototype framework does) – Gerardo Lima Jun 19 '12 at 09:45

11 Answers11

83

Since JSON.stringify has been shipping with some browsers lately, I would suggest using it instead of Prototype’s toJSON. You would then check for window.JSON && window.JSON.stringify and only include the json.org library otherwise (via document.createElement('script')…). To resolve the incompatibilities, use:

if(window.Prototype) {
    delete Object.prototype.toJSON;
    delete Array.prototype.toJSON;
    delete Hash.prototype.toJSON;
    delete String.prototype.toJSON;
}
Raphael Schweikert
  • 18,244
  • 6
  • 55
  • 75
  • No need to check for window.JSON in your own code - the json.org script does this itself – zcrar70 Oct 30 '10 at 15:14
  • That may be so, but then the whole script file has to be loaded even if it won’t be needed. – Raphael Schweikert Oct 31 '10 at 07:57
  • 11
    Actually, the only statement needed to deal with the question is: delete Array.prototype.toJSON – Jean Vincent Jul 07 '11 at 12:37
  • 1
    Thank you so much. The company I work for right now currently still uses prototype in much of our code and this was a life saver for using more modern libraries, otherwise everything was going to break. – krob Apr 05 '13 at 23:11
  • 1
    I've been searching for this answer for DAYS, and posted two different SO questions trying to figure it out. Saw this as a related question as I was typing a third. Thank you so much! – Matthew Herbst Apr 04 '14 at 04:30
  • I just got stung by this with Backbone's Collections, whose toJSON method only returns a subset of the object. – jackocnr May 20 '14 at 00:37
  • wow, first is called Array.toJson, which converts array to string and then JSON.stringify is called on that string. This looks like a bug isn't it? Is this behavior same everywhere, where toJSON is defined? – Tornike Shavishvili Dec 13 '19 at 08:32
82

The function JSON.stringify() defined in ECMAScript 5 and above (Page 201 - the JSON Object, pseudo-code Page 205), uses the function toJSON() when available on objects.

Because Prototype.js (or another library that you are using) defines an Array.prototype.toJSON() function, arrays are first converted to strings using Array.prototype.toJSON() then string quoted by JSON.stringify(), hence the incorrect extra quotes around the arrays.

The solution is therefore straight-forward and trivial (this is a simplified version of Raphael Schweikert's answer):

delete Array.prototype.toJSON

This produces of course side effects on libraries that rely on a toJSON() function property for arrays. But I find this a minor inconvenience considering the incompatibility with ECMAScript 5.

It must be noted that the JSON Object defined in ECMAScript 5 is efficiently implemented in modern browsers and therefore the best solution is to conform to the standard and modify existing libraries.

Jean Vincent
  • 11,995
  • 7
  • 32
  • 24
16

A possible solution which will not affect other Prototype dependencies would be:

var _json_stringify = JSON.stringify;
JSON.stringify = function(value) {
    var _array_tojson = Array.prototype.toJSON;
    delete Array.prototype.toJSON;
    var r=_json_stringify(value);
    Array.prototype.toJSON = _array_tojson;
    return r;
};

This takes care of the Array toJSON incompatibility with JSON.stringify and also retains toJSON functionality as other Prototype libraries may depend on it.

loxx
  • 119
  • 4
akkishore
  • 1,070
  • 1
  • 9
  • 17
  • I used this snippet in a website. It is causing problems. It results into array's toJSON property being undefined. Any pointers on that? – Sourabh Oct 04 '13 at 11:58
  • 1
    Please make sure that your Array.prototype.toJSON is defined before using the above snippet to redefine JSON.stringify. It works fine in my test. – akkishore Oct 05 '13 at 19:15
  • 2
    I wrapped in into `if(typeof Prototype !== 'undefined' && parseFloat(Prototype.Version.substr(0,3)) < 1.7 && typeof Array.prototype.toJSON !== 'undefined')` . It worked. – Sourabh Oct 07 '13 at 05:42
  • 1
    Great. Only until Prototype 1.7 is this an issue. Please upvote :) – akkishore Oct 07 '13 at 16:25
  • 1
    The issue is for versions < 1.7 – Sourabh Oct 08 '13 at 17:27
9

Edit to make a bit more accurate:

The problem key bit of code is in the JSON library from JSON.org (and other implementations of ECMAScript 5's JSON object):

if (value && typeof value === 'object' &&
  typeof value.toJSON === 'function') {
  value = value.toJSON(key);
}

The problem is that the Prototype library extends Array to include a toJSON method, which the JSON object will call in the code above. When the JSON object hits the array value it calls toJSON on the array which is defined in Prototype, and that method returns a string version of the array. Hence, the quotes around the array brackets.

If you delete toJSON from the Array object the JSON library should work properly. Or, just use the JSON library.

Mickael Lherminez
  • 679
  • 1
  • 10
  • 29
Bob
  • 7,851
  • 5
  • 36
  • 48
  • 2
    This is not a bug in the library, because this is the exact way that JSON.stringify() is defined in ECMAScript 5. The problem is with prototype.js and the solution is: delete Array.prototype.toJSON This will have some side effects for prototype toJSON serialization, but I found these minor in regard of the incompatibility that prototype has with ECMAScript 5. – Jean Vincent Jul 07 '11 at 12:35
  • the Prototype library does not extend Object.prototype but Array.prototype, although typeof array in JavaScript also returns "object", they don't have the same "constructor" and prototype. To solve the problem you need to: "delete Array.prototype.toJSON;" – Jean Vincent Jul 07 '11 at 23:45
  • @Jean To be fair, Prototype extends all base native objects, including Object. But ok, I see your point again :) Thanks for helping my answer be better – Bob Jul 08 '11 at 01:52
  • Prototype has stopped extending "Object.prototype" for a long time now (I don't remember which version though) to avoid the for .. in issues. It now only extends static properties of Object (which is much safer) as a namespace : http://api.prototypejs.org/language/Object/ – Jean Vincent Jul 10 '11 at 02:54
  • Jean, actually it is exactly a bug in the library. If an object has toJSON, that must be called and its result must be used, but it should not get quoted. – grr Jun 13 '12 at 11:51
  • Hmm, actually, it seems to be a "bug" in the spec, or a not-quite-thought-out aspect of it anyway. – grr Jun 13 '12 at 12:13
4

I think a better solution would be to include this just after prototype has been loaded

JSON = JSON || {};

JSON.stringify = function(value) { return value.toJSON(); };

JSON.parse = JSON.parse || function(jsonsring) { return jsonsring.evalJSON(true); };

This makes the prototype function available as the standard JSON.stringify() and JSON.parse(), but keeps the native JSON.parse() if it is available, so this makes things more compatible with older browsers.

BenMorel
  • 34,448
  • 50
  • 182
  • 322
  • the JSON.stringify version does not work if the passed in 'value' is an Object. You should do this instead: JSON.stringify = function(value) { return Object.toJSON(value); }; – akkishore Dec 12 '12 at 21:13
2

This is the code I used for the same issue:

function stringify(object){
      var Prototype = window.Prototype
      if (Prototype && Prototype.Version < '1.7' &&
          Array.prototype.toJSON && Object.toJSON){
              return Object.toJSON(object)
      }
      return JSON.stringify(object)
}

You check if Prototype exists, then you check the version. If old version use Object.toJSON (if is defined) in all other cases fallback to JSON.stringify()

Memos
  • 464
  • 3
  • 9
2

I'm not that fluent with Prototype, but I saw this in its docs:

Object.toJSON({"a":[1,2]})

I'm not sure if this would have the same problem the current encoding has, though.

There's also a longer tutorial about using JSON with Prototype.

Powerlord
  • 87,612
  • 17
  • 125
  • 175
1

As people have pointed out, this is due to Prototype.js - specifically versions prior to 1.7. I had a similar situation but had to have code that operated whether Prototype.js was there or not; this means I can't just delete the Array.prototype.toJSON as I'm not sure what relies on it. For that situation this is the best solution I came up with:

function safeToJSON(item){ 
    if ([1,2,3] === JSON.parse(JSON.stringify([1,2,3]))){
        return JSON.stringify(item); //sane behavior
    } else { 
        return item.toJSON(); // Prototype.js nonsense
    }
}

Hopefully it will help someone.

polm23
  • 14,456
  • 7
  • 35
  • 59
1

Here's how I'm dealing with it.

var methodCallString =  Object.toJSON? Object.toJSON(options.jsonMethodCall) :  JSON.stringify(options.jsonMethodCall);
morgancodes
  • 25,055
  • 38
  • 135
  • 187
1

My tolerant solution checks whether Array.prototype.toJSON is harmful for JSON stringify and keeps it when possible to let the surrounding code work as expected:

var dummy = { data: [{hello: 'world'}] }, test = {};

if(Array.prototype.toJSON) {
    try {
        test = JSON.parse(JSON.stringify(dummy));
        if(!test || dummy.data !== test.data) {
            delete Array.prototype.toJSON;
        }
    } catch(e) {
        // there only hope
    }
}
pronebird
  • 12,068
  • 5
  • 54
  • 82
0

If you don't want to kill everything, and have a code that would be okay on most browsers, you could do it this way :

(function (undefined) { // This is just to limit _json_stringify to this scope and to redefine undefined in case it was
  if (true ||typeof (Prototype) !== 'undefined') {
    // First, ensure we can access the prototype of an object.
    // See http://stackoverflow.com/questions/7662147/how-to-access-object-prototype-in-javascript
    if(typeof (Object.getPrototypeOf) === 'undefined') {
      if(({}).__proto__ === Object.prototype && ([]).__proto__ === Array.prototype) {
        Object.getPrototypeOf = function getPrototypeOf (object) {
          return object.__proto__;
        };
      } else {
        Object.getPrototypeOf = function getPrototypeOf (object) {
          // May break if the constructor has been changed or removed
          return object.constructor ? object.constructor.prototype : undefined;
        }
      }
    }

    var _json_stringify = JSON.stringify; // We save the actual JSON.stringify
    JSON.stringify = function stringify (obj) {
      var obj_prototype = Object.getPrototypeOf(obj),
          old_json = obj_prototype.toJSON, // We save the toJSON of the object
          res = null;
      if (old_json) { // If toJSON exists on the object
        obj_prototype.toJSON = undefined;
      }
      res = _json_stringify.apply(this, arguments);
      if (old_json)
        obj_prototype.toJSON = old_json;
      return res;
    };
  }
}.call(this));

This seems complex, but this is complex only to handle most use cases. The main idea is overriding JSON.stringify to remove toJSON from the object passed as an argument, then call the old JSON.stringify, and finally restore it.

Jerska
  • 11,722
  • 4
  • 35
  • 54