JSON.stringify () array bizarreness com Prototype.js

88

Estou tentando descobrir o que há de errado com minha serialização json, tenho a versão atual do meu aplicativo com uma antiga e estou encontrando algumas diferenças surpreendentes na maneira como JSON.stringify () funciona (usando a biblioteca JSON de json.org )

Na versão antiga do meu aplicativo:

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

me dá isso;

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

na nova versão,

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

me dá isso;

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

alguma ideia do que poderia ter mudado para fazer a mesma biblioteca colocar aspas entre os colchetes do array na nova versão?

morgancodes
fonte
4
parece que é um conflito com a biblioteca Prototype, que apresentamos na versão mais recente. Alguma idéia de como stringify um objeto json contendo uma matriz em Prototype?
morgancodes
26
é por isso que as pessoas devem evitar mutilar objetos globais integrados (como o framework de protótipo faz)
Gerardo Lima

Respostas:

81

Como JSON.stringify tem sido enviado com alguns navegadores recentemente, eu sugeriria usá-lo em vez do toJSON do Prototype. Você deve então verificar por window.JSON && window.JSON.stringify e incluir apenas a biblioteca json.org de outra forma (via document.createElement('script')…). Para resolver as incompatibilidades, use:

if(window.Prototype) {
    delete Object.prototype.toJSON;
    delete Array.prototype.toJSON;
    delete Hash.prototype.toJSON;
    delete String.prototype.toJSON;
}
Raphael Schweikert
fonte
Não há necessidade de verificar o window.JSON em seu próprio código - o script json.org faz isso sozinho
zcrar70
Pode ser, mas todo o arquivo de script deve ser carregado, mesmo que não seja necessário.
Raphael Schweikert,
11
Na verdade, a única declaração necessária para lidar com a questão é: delete Array.prototype.toJSON
Jean Vincent
1
Muito obrigado. A empresa para a qual trabalho agora ainda usa protótipo em grande parte do nosso código e isso foi um salva-vidas por usar bibliotecas mais modernas, caso contrário, tudo iria quebrar.
krob
1
Estou procurando essa resposta por DAYS e postei duas perguntas diferentes do SO tentando descobrir. Vi isso como uma questão relacionada enquanto digitava a terceira. Muito obrigado!
Matthew Herbst
78

A função JSON.stringify () definida no ECMAScript 5 e acima (Página 201 - o objeto JSON, pseudo-código Página 205) , usa a função toJSON () quando disponível nos objetos.

Como Prototype.js (ou outra biblioteca que você está usando) define uma função Array.prototype.toJSON (), os arrays são primeiro convertidos em strings usando Array.prototype.toJSON (), em seguida, string citada por JSON.stringify (), daí o citações extras incorretas em torno dos arrays.

A solução é, portanto, direta e trivial (esta é uma versão simplificada da resposta de Raphael Schweikert):

delete Array.prototype.toJSON

Obviamente, isso produz efeitos colaterais em bibliotecas que dependem de uma propriedade de função toJSON () para matrizes. Mas acho isso um pequeno inconveniente, considerando a incompatibilidade com ECMAScript 5.

Deve-se observar que o objeto JSON definido no ECMAScript 5 é implementado de forma eficiente em navegadores modernos e, portanto, a melhor solução é se conformar ao padrão e modificar as bibliotecas existentes.

Jean Vincent
fonte
5
Esta é a resposta mais concisa do que está acontecendo com a citação extra do array.
tmarthal
15

Uma possível solução que não afetará outras dependências do protótipo seria:

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

Isso cuida da incompatibilidade de Array toJSON com JSON.stringify e também retém a funcionalidade toJSON, pois outras bibliotecas de Prototype podem depender dela.

Akkishore
fonte
Eu usei este snippet em um site. Isso está causando problemas. Isso resulta na propriedade toJSON do array indefinida. Alguma indicação sobre isso?
Sourabh
1
Certifique-se de que seu Array.prototype.toJSON esteja definido antes de usar o snippet acima para redefinir JSON.stringify. Funciona bem no meu teste.
akkishore
2
Eu me envolvi em if(typeof Prototype !== 'undefined' && parseFloat(Prototype.Version.substr(0,3)) < 1.7 && typeof Array.prototype.toJSON !== 'undefined'). Funcionou.
Sourabh,
1
Ótimo. Somente até o Protótipo 1.7 isso é um problema.
Vote positivamente
1
O problema é para as versões <1.7
Sourabh
9

Edite para tornar um pouco mais preciso:

O código-chave do problema está na biblioteca JSON de JSON.org (e outras implementações do objeto JSON do ECMAScript 5):

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

O problema é que a biblioteca Prototype estende Array para incluir um método toJSON, que o objeto JSON chamará no código acima. Quando o objeto JSON atinge o valor da matriz, ele chama toJSON na matriz definida em Prototype e esse método retorna uma versão de string da matriz. Portanto, as aspas entre os colchetes da matriz.

Se você excluir toJSON do objeto Array, a biblioteca JSON deverá funcionar corretamente. Ou apenas use a biblioteca JSON.

Prumo
fonte
2
Isso não é um bug na biblioteca, porque esta é a maneira exata que JSON.stringify () é definido no ECMAScript 5. O problema é com prototype.js e a solução é: delete Array.prototype.toJSON Isso terá algum lado efeitos para o protótipo para a serialização JSON, mas achei esses menores em relação à incompatibilidade que o protótipo tem com ECMAScript 5.
Jean Vincent
a biblioteca Prototype não estende Object.prototype, mas Array.prototype, embora typeof array em JavaScript também retorne "objeto", eles não têm o mesmo "construtor" e protótipo. Para resolver o problema, você precisa: "delete Array.prototype.toJSON;"
Jean Vincent
@Jean Para ser justo, Prototype estende todos os objetos nativos básicos, incluindo Object. Mas ok, entendo seu ponto novamente :) Obrigado por ajudar minha resposta a ser melhor
Bob
O Prototype parou de estender "Object.prototype" por um longo tempo (não me lembro qual versão) para evitar problemas for .. in. Agora ele estende apenas as propriedades estáticas do Object (que é muito mais seguro) como um namespace: api.prototypejs.org/language/Object
Jean Vincent
Jean, na verdade é exatamente um bug na biblioteca. Se um objeto tem toJSON, ele deve ser chamado e seu resultado deve ser usado, mas não deve ser colocado entre aspas.
grr
4

Acho que uma solução melhor seria incluir isso logo após o protótipo ser carregado

JSON = JSON || {};

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

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

Isso disponibiliza a função de protótipo como JSON.stringify () e JSON.parse () padrão, mas mantém o JSON.parse () nativo se estiver disponível, portanto, torna as coisas mais compatíveis com navegadores mais antigos.

Benjamin
fonte
a versão JSON.stringify não funciona se o 'valor' passado for um objeto. Em vez disso, você deve fazer isso: JSON.stringify = function (value) {return Object.toJSON (value); };
akkishore
2

Não sou tão fluente com Prototype, mas vi isso em seus documentos :

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

Não tenho certeza se isso teria o mesmo problema que a codificação atual tem, no entanto.

Há também um tutorial mais longo sobre como usar JSON com Prototype.

Powerlord
fonte
2

Este é o código que usei para o mesmo problema:

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

Você verifica se o Prototype existe, então verifica a versão. Se a versão antiga usar Object.toJSON (se estiver definido) em todos os outros casos, fallback para JSON.stringify ()

Memos
fonte
1

É assim que estou lidando com isso.

var methodCallString =  Object.toJSON? Object.toJSON(options.jsonMethodCall) :  JSON.stringify(options.jsonMethodCall);
morgancodes
fonte
1

Minha solução tolerante verifica se Array.prototype.toJSON é prejudicial para JSON stringify e o mantém, quando possível, para permitir que o código circundante funcione conforme o esperado:

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
fonte
1

Como as pessoas apontaram, isso se deve ao Prototype.js - especificamente às versões anteriores a 1.7. Eu tive uma situação semelhante, mas precisava de um código que funcionasse independentemente de o Prototype.js estar lá ou não; isso significa que não posso simplesmente excluir o Array.prototype.toJSON, pois não tenho certeza do que depende dele. Para essa situação, esta é a melhor solução que encontrei:

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

Espero que ajude alguém.

polm23
fonte
0

Se você não quiser matar tudo e tiver um código que funcionaria na maioria dos navegadores, pode fazer desta forma:

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

Isso parece complexo, mas é complexo apenas para lidar com a maioria dos casos de uso. A ideia principal é substituir JSON.stringifypara remover toJSONdo objeto passado como um argumento, chamar o antigo JSON.stringifye finalmente restaurá-lo.

Jerska
fonte