JSON.stringify, évitez TypeError: conversion d'une structure circulaire en JSON
J'ai un gros objet que je veux convertir en JSON et envoyer. Cependant il a la structure circulaire. Je veux lancer toutes les références circulaires existantes et envoyer tout ce qui peut être stringifié. Comment je fais cela ?
Merci.
var obj = {
a: "foo",
b: obj
}
Je veux stringifier obj en:
{"a":"foo"}
19 réponses
Utilisez JSON.stringify
avec un remplaçant personnalisé. Par exemple:
// Demo: Circular reference
var o = {};
o.o = o;
// Note: cache should not be re-used by repeated calls to JSON.stringify.
var cache = [];
JSON.stringify(o, function(key, value) {
if (typeof value === 'object' && value !== null) {
if (cache.indexOf(value) !== -1) {
// Duplicate reference found
try {
// If this value does not reference a parent it can be deduped
return JSON.parse(JSON.stringify(value));
} catch (error) {
// discard key if value cannot be deduped
return;
}
}
// Store value in our collection
cache.push(value);
}
return value;
});
cache = null; // Enable garbage collection
Le remplaçant dans cet exemple n'est pas correct à 100% (selon votre définition de "dupliquer"). Dans le cas suivant, une valeur est ignorée:
var a = {b:1}
var o = {};
o.one = a;
o.two = a;
// one and two point to the same object, but two is discarded:
JSON.stringify(o, ...);
Mais le concept est le suivant: utilisez un remplaçant personnalisé et gardez une trace des valeurs d'objet analysées.
Dans Le Noeud.js, vous pouvez utiliser util.inspecter(objet). Il remplace automatiquement les liens circulaires par "[circulaire]".
Bien qu'il soit intégré (aucune installation n'est requise) , vous devez l'importer
import * as util from 'util' // has no default export
import { inspect } from 'util' // or directly
// or
var util = require('util')
Pour l'utiliser, il suffit d'appeler
console.log(util.inspect(myObject))
Sachez également que vous pouvez passer l'objet options à inspecter (Voir le lien ci-dessus)
inspect(myObject[, options: {showHidden, depth, colors, showProxy, ...moreOptions}])
s " il vous plaît, lire et donner des félicitations aux commentateurs ci-dessous...
Faites juste
npm i --save circular-json
, Puis dans votre fichier js
const JSON = require('circular-json');
...
const json = JSON.stringify(obj);
Vous pouvez aussi faire
const CircularJSON = require('circular-json');
Https://github.com/WebReflection/circular-json
NOTE: je n'ai rien à voir avec ce paquet. Mais je ne les utilise.
J'ai vraiment aimé la solution de Trindaz-plus verbeuse, mais elle avait quelques bugs. Je les ai réparés pour ceux qui l'aiment aussi.
De plus, j'ai ajouté une limite de longueur sur mes objets cache.
Si l'objet que j'imprime est vraiment grand - je veux dire infiniment grand-je veux limiter mon algorithme.
JSON.stringifyOnce = function(obj, replacer, indent){
var printedObjects = [];
var printedObjectKeys = [];
function printOnceReplacer(key, value){
if ( printedObjects.length > 2000){ // browsers will not print more than 20K, I don't see the point to allow 2K.. algorithm will not be fast anyway if we have too many objects
return 'object too long';
}
var printedObjIndex = false;
printedObjects.forEach(function(obj, index){
if(obj===value){
printedObjIndex = index;
}
});
if ( key == ''){ //root element
printedObjects.push(obj);
printedObjectKeys.push("root");
return value;
}
else if(printedObjIndex+"" != "false" && typeof(value)=="object"){
if ( printedObjectKeys[printedObjIndex] == "root"){
return "(pointer to root)";
}else{
return "(see " + ((!!value && !!value.constructor) ? value.constructor.name.toLowerCase() : typeof(value)) + " with key " + printedObjectKeys[printedObjIndex] + ")";
}
}else{
var qualifiedKey = key || "(empty key)";
printedObjects.push(value);
printedObjectKeys.push(qualifiedKey);
if(replacer){
return replacer(key, value);
}else{
return value;
}
}
}
return JSON.stringify(obj, printOnceReplacer, indent);
};
Notez qu'il existe également une méthode JSON.decycle
implémentée par Douglas Crockford. Voir sa
cycle.js . Cela vous permet de stringifier presque n'importe quelle structure standard:
var a = [];
a[0] = a;
a[1] = 123;
console.log(JSON.stringify(JSON.decycle(a)));
// result: '[{"$ref":"$"},123]'.
Vous pouvez également recréer l'objet original avec la méthode retrocycle
. Vous n'avez donc pas besoin de supprimer des cycles d'objets pour les stringifier.
Cependant, cela ne fonctionnera pas pour les nœuds DOM (qui sont la cause typique des cycles dans les cas d'utilisation réels). Par exemple, cela lancera:
var a = [document.body];
console.log(JSON.stringify(JSON.decycle(a)));
J'ai fait une fourchette pour résoudre ce problème (Voir mon cycle .js fourche). Cela devrait fonctionner correctement:
var a = [document.body];
console.log(JSON.stringify(JSON.decycle(a, true)));
Notez que dans ma fourchette JSON.decycle(variable)
fonctionne comme dans l'original et lancera une exception lorsque le variable
contient des nœuds/éléments DOM.
Lorsque vous utilisez JSON.decycle(variable, true)
vous acceptez le fait que le résultat ne sera pas réversible (retrocycle ne recréera pas les nœuds DOM). Les éléments DOM devraient cependant être identifiables dans une certaine mesure. Par exemple, si un div
élément possède un id il sera remplacé avec une chaîne "div#id-of-the-element"
.
Je recommande de vérifier JSON-stringify-safe de @isaacs - il est utilisé dans NPM.
BTW-si vous n'utilisez pas Node.js, vous pouvez simplement copier et coller les lignes 4-27 de la partie pertinente du code source .
Pour installer:
$ npm install json-stringify-safe --save
À utiliser:
// Require the thing
var stringify = require('json-stringify-safe');
// Take some nasty circular object
var theBigNasty = {
a: "foo",
b: theBigNasty
};
// Then clean it up a little bit
var sanitized = JSON.parse(stringify(theBigNasty));
Cela donne:
{
a: 'foo',
b: '[Circular]'
}
Notez que, tout comme avec le JSON vanille.fonction stringify comme @ Rob W mentionné, vous pouvez également personnaliser le comportement de désinfection en passant dans une fonction "replacer" comme deuxième argument de
stringify()
. Si vous avez besoin d'un exemple simple de la façon de le faire, je viens d'écrire un remplaçant personnalisé qui contraint les erreurs, les expressions rationnelles et les fonctions dans des chaînes lisibles par l'homme ici.
La réponse de@RobW est correcte, mais c'est plus performant ! Parce qu'il utilise un HashMap / set:
const customStringify = function (v) {
const cache = new Set();
return JSON.stringify(v, function (key, value) {
if (typeof value === 'object' && value !== null) {
if (cache.has(value)) {
// Circular reference found, discard key
return;
}
// Store value in our set
cache.add(value);
}
return value;
});
};
Pour les futurs googlers cherchant une solution à ce problème lorsque vous ne connaissez pas les clés de toutes les références circulaires, vous pouvez utiliser un wrapper autour du JSON.fonction stringify pour exclure les références circulaires. Voir un exemple de script à https://gist.github.com/4653128.
La solution se résume essentiellement à garder une référence aux objets précédemment imprimés dans un tableau, et à vérifier cela dans une fonction de remplacement avant de renvoyer une valeur. C'est plus constrictif que d'exclure seulement les références circulaires, car il exclut également l'impression d'un objet deux fois, dont l'un des effets secondaires est d'éviter les références circulaires.
Exemple d'emballage:
function stringifyOnce(obj, replacer, indent){
var printedObjects = [];
var printedObjectKeys = [];
function printOnceReplacer(key, value){
var printedObjIndex = false;
printedObjects.forEach(function(obj, index){
if(obj===value){
printedObjIndex = index;
}
});
if(printedObjIndex && typeof(value)=="object"){
return "(see " + value.constructor.name.toLowerCase() + " with key " + printedObjectKeys[printedObjIndex] + ")";
}else{
var qualifiedKey = key || "(empty key)";
printedObjects.push(value);
printedObjectKeys.push(qualifiedKey);
if(replacer){
return replacer(key, value);
}else{
return value;
}
}
}
return JSON.stringify(obj, printOnceReplacer, indent);
}
Utilisez le JSON.méthode stringify avec un remplaçant. Lisez cette documentation pour plus d'informations. http://msdn.microsoft.com/en-us/library/cc836459%28v=vs.94%29.aspx
var obj = {
a: "foo",
b: obj
}
var replacement = {"b":undefined};
alert(JSON.stringify(obj,replacement));
Trouver un moyen de remplir le tableau de remplacement avec des références cycliques. Vous pouvez utiliser la méthode typeof pour trouver si une propriété est de type 'object' (référence ) et une vérification d'égalité exacte ( = = = ) pour vérifier la référence circulaire.
var a={b:"b"};
a.a=a;
JSON.stringify(preventCircularJson(a));
Évalue à:
"{"b":"b","a":"CIRCULAR_REFERENCE_REMOVED"}"
Avec la fonction:
/**
* Traverses a javascript object, and deletes all circular values
* @param source object to remove circular references from
* @param censoredMessage optional: what to put instead of censored values
* @param censorTheseItems should be kept null, used in recursion
* @returns {undefined}
*/
function preventCircularJson(source, censoredMessage, censorTheseItems) {
//init recursive value if this is the first call
censorTheseItems = censorTheseItems || [source];
//default if none is specified
censoredMessage = censoredMessage || "CIRCULAR_REFERENCE_REMOVED";
//values that have allready apeared will be placed here:
var recursiveItems = {};
//initaite a censored clone to return back
var ret = {};
//traverse the object:
for (var key in source) {
var value = source[key]
if (typeof value == "object") {
//re-examine all complex children again later:
recursiveItems[key] = value;
} else {
//simple values copied as is
ret[key] = value;
}
}
//create list of values to censor:
var censorChildItems = [];
for (var key in recursiveItems) {
var value = source[key];
//all complex child objects should not apear again in children:
censorChildItems.push(value);
}
//censor all circular values
for (var key in recursiveItems) {
var value = source[key];
var censored = false;
censorTheseItems.forEach(function (item) {
if (item === value) {
censored = true;
}
});
if (censored) {
//change circular values to this
value = censoredMessage;
} else {
//recursion:
value = preventCircularJson(value, censoredMessage, censorChildItems.concat(censorTheseItems));
}
ret[key] = value
}
return ret;
}
Je sais que c'est une vieille question, mais je voudrais suggérer un paquet NPM que j'ai créé appelé smart-circular, qui fonctionne différemment des autres façons proposées. C'est particulièrement utile si vous utilisez des objets grands et profonds.
Certaines fonctionnalités sont:
Remplacer des références circulaires ou simplement des structures répétées à l'intérieur de l'objet par le chemin menant à sa première occurrence (pas seulement la chaîne [circulaire]);
En regardant pour les circularités dans une recherche en largeur, le paquet garantit que ce chemin est aussi petit que possible, ce qui est important lorsqu'il s'agit d'objets très grands et profonds, où les chemins peuvent être agaçants et difficiles à suivre (le remplacement personnalisé dans JSON.stringify fait un DFS);
-
Permet des remplacements personnalisés, pratiques pour simplifier ou ignorer des parties moins importantes de l'objet;
Enfin, les chemins sont écrits exactement de la manière nécessaire pour accéder au champ référencé, ce qui peut vous aider à déboguer.
Je résous ce problème comme ceci:
var util = require('util');
// Our circular object
var obj = {foo: {bar: null}, a:{a:{a:{a:{a:{a:{a:{hi: 'Yo!'}}}}}}}};
obj.foo.bar = obj;
// Generate almost valid JS object definition code (typeof string)
var str = util.inspect(b, {depth: null});
// Fix code to the valid state (in this example it is not required, but my object was huge and complex, and I needed this for my case)
str = str
.replace(/<Buffer[ \w\.]+>/ig, '"buffer"')
.replace(/\[Function]/ig, 'function(){}')
.replace(/\[Circular]/ig, '"Circular"')
.replace(/\{ \[Function: ([\w]+)]/ig, '{ $1: function $1 () {},')
.replace(/\[Function: ([\w]+)]/ig, 'function $1(){}')
.replace(/(\w+): ([\w :]+GMT\+[\w \(\)]+),/ig, '$1: new Date("$2"),')
.replace(/(\S+): ,/ig, '$1: null,');
// Create function to eval stringifyed code
var foo = new Function('return ' + str + ';');
// And have fun
console.log(JSON.stringify(foo(), null, 4));
Le deuxième argument de JSON.stringify () aussi vous permet de spécifier un tableau de noms de clés qui doivent être conservés à partir de chaque objet qu'il rencontre dans vos données. Cela peut ne pas fonctionner pour tous les cas d'utilisation, mais c'est une solution beaucoup plus simple.
Https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
var obj = {
a: "foo",
b: this
}
var json = JSON.stringify(obj, ['a']);
console.log(json);
// {"a":"foo"}
Remarque: étrangement, la définition d'objet de L'OP ne lance pas d'erreur de référence circulaire dans le dernière Chrome ou Firefox. La définition dans cette réponse a été modifiée de sorte que a fait jeter une erreur.
Une autre solution pour résoudre ce problème, avec ce genre d'objets que l'utilisation de cette bibliothèque
Https://github.com/ericmuyser/stringy
C'est simple et vous pouvez en quelques étapes simples résoudre cela.
J'ai trouvé la bibliothèque circular-json sur github et cela a bien fonctionné pour mon problème.
Quelques bonnes fonctionnalités que j'ai trouvées utiles:
- prend en charge l'utilisation multi-plateforme mais je ne l'ai testé qu'avec node.js jusqu'à présent.
- L'API est la même, donc tout ce que vous devez faire est de l'inclure et de l'utiliser comme remplacement JSON.
- Il a sa propre méthode d'analyse afin que vous puissiez convertir les données sérialisées 'circulaires'en objet.
Sur la Base des autres réponses, je me retrouve avec le code suivant. Cela fonctionne assez bien avec des références circulaires, des objets avec des constructeurs personnalisés.
De l'objet donné à sérialiser,
- Cache tout l'objet que vous rencontrez en traversant l'objet et attribue à chacun d'eux un hashID unique (un nombre d'incrémentation automatique fonctionne également)
- Une fois qu'une référence circulaire est trouvée, marquez ce champ dans le nouvel objet comme circulaire et stockez le hashID de l'objet d'origine comme un attribut.
Lien Github - DecycledJSON
DJSHelper = {};
DJSHelper.Cache = [];
DJSHelper.currentHashID = 0;
DJSHelper.ReviveCache = [];
// DOES NOT SERIALIZE FUNCTION
function DJSNode(name, object, isRoot){
this.name = name;
// [ATTRIBUTES] contains the primitive fields of the Node
this.attributes = {};
// [CHILDREN] contains the Object/Typed fields of the Node
// All [CHILDREN] must be of type [DJSNode]
this.children = []; //Array of DJSNodes only
// If [IS-ROOT] is true reset the Cache and currentHashId
// before encoding
isRoot = typeof isRoot === 'undefined'? true:isRoot;
this.isRoot = isRoot;
if(isRoot){
DJSHelper.Cache = [];
DJSHelper.currentHashID = 0;
// CACHE THE ROOT
object.hashID = DJSHelper.currentHashID++;
DJSHelper.Cache.push(object);
}
for(var a in object){
if(object.hasOwnProperty(a)){
var val = object[a];
if (typeof val === 'object') {
// IF OBJECT OR NULL REF.
/***************************************************************************/
// DO NOT REMOVE THE [FALSE] AS THAT WOULD RESET THE [DJSHELPER.CACHE]
// AND THE RESULT WOULD BE STACK OVERFLOW
/***************************************************************************/
if(val !== null) {
if (DJSHelper.Cache.indexOf(val) === -1) {
// VAL NOT IN CACHE
// ADD THE VAL TO CACHE FIRST -> BEFORE DOING RECURSION
val.hashID = DJSHelper.currentHashID++;
//console.log("Assigned", val.hashID, "to", a);
DJSHelper.Cache.push(val);
if (!(val instanceof Array)) {
// VAL NOT AN [ARRAY]
try {
this.children.push(new DJSNode(a, val, false));
} catch (err) {
console.log(err.message, a);
throw err;
}
} else {
// VAL IS AN [ARRAY]
var node = new DJSNode(a, {
array: true,
hashID: val.hashID // HashID of array
}, false);
val.forEach(function (elem, index) {
node.children.push(new DJSNode("elem", {val: elem}, false));
});
this.children.push(node);
}
} else {
// VAL IN CACHE
// ADD A CYCLIC NODE WITH HASH-ID
this.children.push(new DJSNode(a, {
cyclic: true,
hashID: val.hashID
}, false));
}
}else{
// PUT NULL AS AN ATTRIBUTE
this.attributes[a] = 'null';
}
} else if (typeof val !== 'function') {
// MUST BE A PRIMITIVE
// ADD IT AS AN ATTRIBUTE
this.attributes[a] = val;
}
}
}
if(isRoot){
DJSHelper.Cache = null;
}
this.constructorName = object.constructor.name;
}
DJSNode.Revive = function (xmlNode, isRoot) {
// Default value of [isRoot] is True
isRoot = typeof isRoot === 'undefined'?true: isRoot;
var root;
if(isRoot){
DJSHelper.ReviveCache = []; //Garbage Collect
}
if(window[xmlNode.constructorName].toString().indexOf('[native code]') > -1 ) {
// yep, native in the browser
if(xmlNode.constructorName == 'Object'){
root = {};
}else{
return null;
}
}else {
eval('root = new ' + xmlNode.constructorName + "()");
}
//CACHE ROOT INTO REVIVE-CACHE
DJSHelper.ReviveCache[xmlNode.attributes.hashID] = root;
for(var k in xmlNode.attributes){
// PRIMITIVE OR NULL REF FIELDS
if(xmlNode.attributes.hasOwnProperty(k)) {
var a = xmlNode.attributes[k];
if(a == 'null'){
root[k] = null;
}else {
root[k] = a;
}
}
}
xmlNode.children.forEach(function (value) {
// Each children is an [DJSNode]
// [Array]s are stored as [DJSNode] with an positive Array attribute
// So is value
if(value.attributes.array){
// ITS AN [ARRAY]
root[value.name] = [];
value.children.forEach(function (elem) {
root[value.name].push(elem.attributes.val);
});
//console.log("Caching", value.attributes.hashID);
DJSHelper.ReviveCache[value.attributes.hashID] = root[value.name];
}else if(!value.attributes.cyclic){
// ITS AN [OBJECT]
root[value.name] = DJSNode.Revive(value, false);
//console.log("Caching", value.attributes.hashID);
DJSHelper.ReviveCache[value.attributes.hashID] = root[value.name];
}
});
// [SEPARATE ITERATION] TO MAKE SURE ALL POSSIBLE
// [CYCLIC] REFERENCES ARE CACHED PROPERLY
xmlNode.children.forEach(function (value) {
// Each children is an [DJSNode]
// [Array]s are stored as [DJSNode] with an positive Array attribute
// So is value
if(value.attributes.cyclic){
// ITS AND [CYCLIC] REFERENCE
root[value.name] = DJSHelper.ReviveCache[value.attributes.hashID];
}
});
if(isRoot){
DJSHelper.ReviveCache = null; //Garbage Collect
}
return root;
};
DecycledJSON = {};
DecycledJSON.stringify = function (obj) {
return JSON.stringify(new DJSNode("root", obj));
};
DecycledJSON.parse = function (json, replacerObject) {
// use the replacerObject to get the null values
return DJSNode.Revive(JSON.parse(json));
};
DJS = DecycledJSON;
Exemple D'Utilisation 1:
var obj = {
id:201,
box: {
owner: null,
key: 'storm'
},
lines:[
'item1',
23
]
};
console.log(obj); // ORIGINAL
// SERIALIZE AND THEN PARSE
var jsonObj = DJS.stringify(obj);
console.log(DJS.parse(jsonObj));
Exemple D'Utilisation 2:
// PERSON OBJECT
function Person() {
this.name = null;
this.child = null;
this.dad = null;
this.mom = null;
}
var Dad = new Person();
Dad.name = 'John';
var Mom = new Person();
Mom.name = 'Sarah';
var Child = new Person();
Child.name = 'Kiddo';
Dad.child = Mom.child = Child;
Child.dad = Dad;
Child.mom = Mom;
console.log(Child); // ORIGINAL
// SERIALIZE AND THEN PARSE
var jsonChild = DJS.stringify(Child);
console.log(DJS.parse(jsonChild));
Je sais que cette question Est ancienne et a beaucoup de bonnes réponses mais je poste cette réponse à cause de sa nouvelle saveur (es5+)
Object.defineProperties(JSON, {
refStringify: {
value: function(obj) {
let objMap = new Map();
let stringified = JSON.stringify(obj,
function(key, value) {
// only for objects
if (typeof value == 'object') {
// If has the value then return a reference to it
if (objMap.has(value))
return objMap.get(value);
objMap.set(value, `ref${objMap.size + 1}`);
}
return value;
});
return stringified;
}
},
refParse: {
value: function(str) {
let parsed = JSON.parse(str);
let objMap = _createObjectMap(parsed);
objMap.forEach((value, key) => _replaceKeyWithObject(value, key));
return parsed;
}
},
});
// *************************** Example
let a = {
b: 32,
c: {
get a() {
return a;
},
get c() {
return a.c;
}
}
};
let stringified = JSON.refStringify(a);
let parsed = JSON.refParse(stringified, 2);
console.log(parsed, JSON.refStringify(parsed));
// *************************** /Example
// *************************** Helper
function _createObjectMap(obj) {
let objMap = new Map();
JSON.stringify(obj, (key, value) => {
if (typeof value == 'object') {
if (objMap.has(value))
return objMap.get(value);
objMap.set(value, `ref${objMap.size + 1}`);
}
return value;
});
return objMap;
}
function _replaceKeyWithObject(key, obj, replaceWithObject = obj) {
Object.keys(obj).forEach(k => {
let val = obj[k];
if (val == key)
return (obj[k] = replaceWithObject);
if (typeof val == 'object' && val != replaceWithObject)
_replaceKeyWithObject(key, val, replaceWithObject);
});
}
Essayez ceci:
var obj = {
a: "foo",
b: obj
};
var circular_replacer = (value) => {
var seen = [];
if (value != null && typeof value == "object") {
if (seen.indexOf(value) >= 0) return;
seen.push(value);
}
return value;
};
obj = circular_replacer(obj);
Si
console.log(JSON.stringify(object));
Résultats dans un
TypeError: valeur d'objet cyclique
Ensuite, vous pouvez imprimer comme ceci:
var output = '';
for (property in object) {
output += property + ': ' + object[property]+'; ';
}
console.log(output);