Bizarrerie du tableau JSON.stringify () avec Prototype.js

88

J'essaie de comprendre ce qui ne va pas avec ma sérialisation json, j'ai la version actuelle de mon application avec une ancienne et je trouve des différences surprenantes dans le fonctionnement de JSON.stringify () (en utilisant la bibliothèque JSON de json.org ).

Dans l'ancienne version de mon application:

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

me donne ceci;

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

dans la nouvelle version,

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

me donne ceci;

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

une idée de ce qui aurait pu changer pour que la même bibliothèque place des guillemets autour des crochets du tableau dans la nouvelle version?

Morgancodes
la source
4
Il semble que ce soit un conflit avec la bibliothèque Prototype, que nous avons introduite dans la nouvelle version. Des idées sur la manière de stringifier un objet json contenant un tableau sous Prototype?
morgancodes le
26
c'est pourquoi les gens devraient s'abstenir de manipuler des objets intégrés globaux (comme le fait le framework prototype)
Gerardo Lima

Réponses:

81

Étant donné que JSON.stringify a été livré avec certains navigateurs récemment, je suggère de l'utiliser à la place du toJSON de Prototype. Vous vérifieriez alors window.JSON && window.JSON.stringify et n'incluriez que la bibliothèque json.org sinon (via document.createElement('script')…). Pour résoudre les incompatibilités, utilisez:

if(window.Prototype) {
    delete Object.prototype.toJSON;
    delete Array.prototype.toJSON;
    delete Hash.prototype.toJSON;
    delete String.prototype.toJSON;
}
Raphael Schweikert
la source
Pas besoin de vérifier window.JSON dans votre propre code - le script json.org le fait lui
zcrar70
C'est peut-être le cas, mais alors le fichier de script entier doit être chargé même s'il n'est pas nécessaire.
Raphael Schweikert
11
En fait, la seule déclaration nécessaire pour traiter la question est: supprimer Array.prototype.toJSON
Jean Vincent
1
Merci beaucoup. La société pour laquelle je travaille actuellement utilise encore un prototype dans une grande partie de notre code et cela a sauvé la vie de l'utilisation de bibliothèques plus modernes, sinon tout allait se casser.
krob
1
J'ai cherché cette réponse pendant DAYS, et j'ai posté deux questions SO différentes pour essayer de la comprendre. J'ai vu cela comme une question connexe alors que j'en tapais une troisième. Merci beaucoup!
Matthew Herbst
78

La fonction JSON.stringify () définie dans ECMAScript 5 et supérieur (Page 201 - l'objet JSON, pseudo-code Page 205) , utilise la fonction toJSON () lorsqu'elle est disponible sur les objets.

Étant donné que Prototype.js (ou une autre bibliothèque que vous utilisez) définit une fonction Array.prototype.toJSON (), les tableaux sont d'abord convertis en chaînes à l'aide de Array.prototype.toJSON () puis une chaîne citée par JSON.stringify (), d'où le guillemets supplémentaires incorrects autour des tableaux.

La solution est donc simple et triviale (il s'agit d'une version simplifiée de la réponse de Raphael Schweikert):

delete Array.prototype.toJSON

Cela produit bien sûr des effets secondaires sur les bibliothèques qui reposent sur une propriété de fonction toJSON () pour les tableaux. Mais je trouve cela un inconvénient mineur compte tenu de l'incompatibilité avec ECMAScript 5.

Il faut noter que l'objet JSON défini dans ECMAScript 5 est efficacement implémenté dans les navigateurs modernes et que la meilleure solution est donc de se conformer au standard et de modifier les bibliothèques existantes.

Jean Vincent
la source
5
C'est la réponse la plus concise de ce qui se passe avec la citation supplémentaire du tableau.
tmarthal
15

Une solution possible qui n'affectera pas les autres dépendances de Prototype serait:

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;
};

Cela prend en charge l'incompatibilité Array toJSON avec JSON.stringify et conserve également la fonctionnalité toJSON car d'autres bibliothèques de prototypes peuvent en dépendre.

Akkishore
la source
J'ai utilisé cet extrait dans un site Web. Cela pose des problèmes. Il en résulte que la propriété toJSON du tableau n'est pas définie. Des conseils à ce sujet?
Sourabh
1
Veuillez vous assurer que votre Array.prototype.toJSON est défini avant d'utiliser l'extrait de code ci-dessus pour redéfinir JSON.stringify. Cela fonctionne bien dans mon test.
akkishore
2
Je me suis enveloppé dans if(typeof Prototype !== 'undefined' && parseFloat(Prototype.Version.substr(0,3)) < 1.7 && typeof Array.prototype.toJSON !== 'undefined'). Ça a marché.
Sourabh
1
Génial. Ce n'est que jusqu'à Prototype 1.7 que c'est un problème. S'il vous plaît upvote :)
akkishore
1
Le problème concerne les versions <1.7
Sourabh
9

Modifiez pour rendre un peu plus précis:

Le bit de code clé du problème se trouve dans la bibliothèque JSON de JSON.org (et d'autres implémentations de l'objet JSON d'ECMAScript 5):

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

Le problème est que la bibliothèque Prototype étend Array pour inclure une méthode toJSON, que l'objet JSON appellera dans le code ci-dessus. Lorsque l'objet JSON atteint la valeur du tableau, il appelle toJSON sur le tableau qui est défini dans Prototype, et cette méthode retourne une version chaîne du tableau. Par conséquent, les guillemets autour des crochets du tableau.

Si vous supprimez toJSON de l'objet Array, la bibliothèque JSON devrait fonctionner correctement. Ou, utilisez simplement la bibliothèque JSON.

Bob
la source
2
Ce n'est pas un bogue dans la bibliothèque, car c'est la manière exacte dont JSON.stringify () est défini dans ECMAScript 5. Le problème vient de prototype.js et la solution est: supprimer Array.prototype.toJSON Cela aura un côté effets pour la sérialisation prototype toJSON, mais j'ai trouvé ces mineurs en ce qui concerne l'incompatibilité du prototype avec ECMAScript 5.
Jean Vincent
la bibliothèque Prototype n'étend pas Object.prototype mais Array.prototype, bien que typeof array en JavaScript retourne également "object", ils n'ont pas le même "constructeur" et prototype. Pour résoudre le problème, vous devez: "supprimer Array.prototype.toJSON;"
Jean Vincent
@Jean Pour être honnête, Prototype étend tous les objets natifs de base, y compris Object. Mais ok, je vois votre point à nouveau :) Merci d'avoir aidé ma réponse à être meilleure
Bob
Prototype a cessé d'étendre "Object.prototype" depuis longtemps maintenant (je ne me souviens pas de quelle version) pour éviter les problèmes for .. in. Il étend maintenant uniquement les propriétés statiques de Object (ce qui est beaucoup plus sûr) en tant qu'espace de
Jean Vincent
Jean, en fait c'est exactement un bug dans la bibliothèque. Si un objet a toJSON, il doit être appelé et son résultat doit être utilisé, mais il ne doit pas être cité.
grr
4

Je pense qu'une meilleure solution serait de l'inclure juste après le chargement du prototype

JSON = JSON || {};

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

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

Cela rend la fonction prototype disponible en tant que JSON.stringify () et JSON.parse () standard, mais conserve le JSON.parse () natif s'il est disponible, ce qui rend les choses plus compatibles avec les navigateurs plus anciens.

Benjamin
la source
la version JSON.stringify ne fonctionne pas si la 'valeur' ​​passée est un objet. Vous devriez le faire à la place: JSON.stringify = function (value) {return Object.toJSON (value); };
akkishore
2

Je ne maîtrise pas très bien Prototype, mais j'ai vu cela dans ses documents :

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

Je ne suis pas sûr que cela pose le même problème que l'encodage actuel.

Il existe également un didacticiel plus long sur l'utilisation de JSON avec Prototype.

Powerlord
la source
2

C'est le code que j'ai utilisé pour le même problème:

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)
}

Vous vérifiez si Prototype existe, puis vous vérifiez la version. Si l'ancienne version utilise Object.toJSON (si est défini) dans tous les autres cas, retournez à JSON.stringify ()

Mémos
la source
1

Voici comment je gère ça.

var methodCallString =  Object.toJSON? Object.toJSON(options.jsonMethodCall) :  JSON.stringify(options.jsonMethodCall);
Morgancodes
la source
1

Ma solution tolérante vérifie si Array.prototype.toJSON est nocif pour JSON stringify et le conserve lorsque cela est possible pour laisser le code environnant fonctionner comme prévu:

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
    }
}
Ben Sinclair
la source
1

Comme les gens l'ont souligné, cela est dû à Prototype.js - en particulier aux versions antérieures à la 1.7. J'avais une situation similaire mais je devais avoir un code qui fonctionnait, que Prototype.js soit là ou non; cela signifie que je ne peux pas simplement supprimer Array.prototype.toJSON car je ne suis pas sûr de ce qui en dépend. Pour cette situation, c'est la meilleure solution que j'ai trouvée:

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
    }
}

J'espère que cela aidera quelqu'un.

polm23
la source
0

Si vous ne voulez pas tout tuer et avoir un code qui convient à la plupart des navigateurs, vous pouvez le faire de cette façon:

(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));

Cela semble complexe, mais cela n'est complexe que pour gérer la plupart des cas d'utilisation. L'idée principale est JSON.stringifyde supprimer toJSONde l'objet passé en argument, puis d'appeler l'ancien JSON.stringifyet enfin de le restaurer.

Jerska
la source