203

I have an object (parse tree) that contains child nodes which are references to other nodes.

I'd like to serialize this object, using JSON.stringify(), but I get

TypeError: cyclic object value

because of the constructs I mentioned.

How could I work around this? It does not matter to me whether these references to other nodes are represented or not in the serialized object.

On the other hand, removing these properties from the object when they are being created seems tedious and I wouldn't want to make changes to the parser (narcissus).

georgeawg
49k13 gold badges77 silver badges98 bronze badges
asked Feb 21, 2012 at 17:27
7
  • 2
    We can't help you without some code. Please post the relevant bits of your object and/or JSON output along with the JS you use to serialise it. Commented Feb 21, 2012 at 17:32
  • 1
    are you able to add some prefix to those properties which are internal references? Commented Feb 21, 2012 at 17:33
  • @Loic It would be valuable to have Douglas Crockford's cycle.js as an answer here, since it's the most appropriate solution for a lot of cases. It seems appropriate for you to post that answer, since you're the first one to reference it (in your comment below). If you don't feel like posting it as an answer yourself, I will eventually do so. Commented May 24, 2013 at 14:23
  • 4
    Possible duplicate of JSON.stringify, avoid TypeError: Converting circular structure to JSON Commented Feb 12, 2016 at 8:26
  • 2
    I wish JSON would be smarter, or an easier way of solving this. The solutions are too troublesome for simple(!) debugging purposes imo. Commented Sep 6, 2019 at 11:39

8 Answers 8

288

Use the second parameter of stringify, the replacer function, to exclude already serialized objects:

var seen = [];
JSON.stringify(obj, function(key, val) {
 if (val != null && typeof val == "object") {
 if (seen.indexOf(val) >= 0) {
 return;
 }
 seen.push(val);
 }
 return val;
});

http://jsfiddle.net/mH6cJ/38/

As correctly pointed out in other comments, this code removes every "seen" object, not only "recursive" ones.

For example, for:

a = {x:1};
obj = [a, a];

the result will be incorrect. If your structure is like this, you might want to use Crockford's decycle or this (simpler) function which just replaces recursive references with nulls:

function decycle(obj, stack = []) {
 if (!obj || typeof obj !== 'object')
 return obj;
 
 if (stack.includes(obj))
 return null;
 let s = stack.concat([obj]);
 return Array.isArray(obj)
 ? obj.map(x => decycle(x, s))
 : Object.fromEntries(
 Object.entries(obj)
 .map(([k, v]) => [k, decycle(v, s)]));
}
//
let a = {b: [1, 2, 3]}
a.b.push(a);
console.log(JSON.stringify(decycle(a)))

answered Feb 21, 2012 at 17:41
Sign up to request clarification or add additional context in comments.

7 Comments

aaah nice! Thanks, I'm going to try this. I found a solution created by Douglas Crockford (github.com/douglascrockford/JSON-js/blob/master/cycle.js ), but as I am unsure of the license that goes with it, the easy solution you describe would be perfect!
@LoicDuros The license is "public domain". Meaning, you can do anything you want with it.
this code produces cycling loops, beware of using, very potential crashes your app. needs correct semicolons and is not useable on event objects!
This removes more than just cyclic references - it simply removes anything that appears more than once. Unless the object that has already been serialized is a "parent" of the new object, you shouldn't delete it
Good answer! I modified this a little, changed the function into a recursive function, so that child-objects would get cloned the way parent objects are cloned.
|
17

This is kind of an alternate-answer, but since what a lot of people will come here for is debugging their circular objects and there's not really a great way to do that without pulling in a bunch of code, here goes.

One feature that's not as well-known as JSON.stringify() is console.table(). Simply call console.table(whatever);, and it will log the variable in the console in tabular format, making it rather quite easy and convenient to peruse the variable's contents.

answered Jul 10, 2020 at 17:26

Comments

12

Here is an example of a data structure with cyclic references: toolshedCY

function makeToolshed(){
 var nut = {name: 'nut'}, bolt = {name: 'bolt'};
 nut.needs = bolt; bolt.needs = nut;
 return { nut: nut, bolt: bolt };
}

When you wish to KEEP the cyclic references (restore them when you deserialize, instead of "nuking" them), you have 2 choices, which I'll compare here. First is Douglas Crockford's cycle.js, second is my siberia package. Both work by first "decycling" the object, i.e., constructing another object (without any cyclic references) "containing the same information."

Mr. Crockford goes first:

JSON.decycle(makeToolshed())

JSON_decycleMakeToolshed

As you see, the nested structure of JSON is retained, but there is a new thing, which is objects with the special $ref property. Let's see how that works.

root = makeToolshed();
[root.bolt === root.nut.needs, root.nut.needs.needs === root.nut]; // retutrns [true,true]

The dollar sign stands for the root. .bolt having $ref tells us that .bolt is an "already seen" object, and the value of that special property (here, the string $["nut"]["needs"]) tells us where, see first === above. Likewise for second $ref and the second === above.

Let's use a suitable deep equality test (namely Anders Kaseorg's deepGraphEqual function from accepted answer to this question) to see if cloning works.

root = makeToolshed();
clone = JSON.retrocycle(JSON.decycle(root));
deepGraphEqual(root, clone) // true
serialized = JSON.stringify(JSON.decycle(root));
clone2 = JSON.retrocycle(JSON.parse(serialized));
deepGraphEqual(root, clone2); // true

Now, siberia:

JSON.Siberia.forestify(makeToolshed())

JSON_Siberia_forestify_makeToolshed

Siberia does not try to mimic "classic" JSON, no nested structure. The object graph is described in a "flat" manner. Each node of the object graph is turned into a flat tree (plain key value pair list with integer-only values), which is an entry in .forest. At index zero, we find the root object, at higher indices, we find the other nodes of the object graph, and negative values (of some key of some tree of the forest) point to the atoms array, (which is typed via the types array, but we'll skip the typing details here). All terminal nodes are in the atoms table, all non-terminal nodes are in the forest table, and you can see right away how many nodes the object graph has, namely forest.length. Let's test if it works:

root = makeToolshed();
clone = JSON.Siberia.unforestify(JSON.Siberia.forestify(root));
deepGraphEqual(root, clone); // true
serialized = JSON.Siberia.stringify(JSON.Siberia.forestify(root));
clone2 = JSON.Siberia.unforestify(JSON.Siberia.unstringify(serialized));
deepGraphEqual(root, clone2); // true

comparison

will add section later.

note

I'm currently refactoring the package. Central ideas and algorithms are staying the same, but the new version will be easier to use, the top level API will be different. I will very soon archive siberia and present the refactored version, which I'll call objectgraph. Stay tuned, it will happen this month (August 2020)

ah, and ultra short version for the comparison. For a "pointer", I need as much space as an integer takes, since my "pointers to already seen nodes" (as a matter of fact, to all nodes, already seen or not) are just integers. In Mr. Crockford's version, amount needed to store a "pointer" is bounded only by the size of the object graph. That makes the worst case complexity of Mr. Crockford's version extremely horrible. Mr. Crockford gave us "another Bubblesort". I'm not kidding you. It's that bad. If you don't believe it, there are tests, you can find them starting from the readme of the package (will transform them to be benchmark.js compliant also this month, Aug 2020)

answered Nov 12, 2019 at 2:04

1 Comment

I installed cycle.js via npm i cycle but I get a TypeError: JSON.decycle is not a function. Do I need to import the decycle method? If so, how do I import it?
4

much saver and it shows where an cycle object was.

<script>
var jsonify=function(o){
 var seen=[];
 var jso=JSON.stringify(o, function(k,v){
 if (typeof v =='object') {
 if ( !seen.indexOf(v) ) { return '__cycle__'; }
 seen.push(v);
 } return v;
 });
 return jso;
};
var obj={
 g:{
 d:[2,5],
 j:2
 },
 e:10
};
obj.someloopshere = [
 obj.g,
 obj,
 { a: [ obj.e, obj ] }
];
console.log('jsonify=',jsonify(obj));
</script>

produces

jsonify = {"g":{"d":[2,5],"j":2},"e":10,"someloopshere":[{"d":[2,5],"j":2},"__cycle__",{"a":[10,"__cycle__"]}]}
answered Apr 22, 2013 at 18:05

2 Comments

but there is still an issue with this code if someone would build an object with obj.b=this' if someone knows how to prevent very long calcs made of a wrong given scope with this would be nice to see here
This should be seen.indexOf(v) != -1
3

I've created an GitHub Gist which is able to detect cyclic structures and also de- and encodes them: https://gist.github.com/Hoff97/9842228

To transform just use JSONE.stringify/JSONE.parse. It also de- and encodes functions. If you want to disable this just remove lines 32-48 and 61-85.

var strg = JSONE.stringify(cyclicObject);
var cycObject = JSONE.parse(strg);

You can find an example fiddle here:

http://jsfiddle.net/hoff97/7UYd4/

answered Mar 28, 2014 at 21:31

Comments

2

I create too a github project that can serialize cyclic object and restore the class if you save it in the serializename attribute like a String

var d={}
var a = {b:25,c:6,enfant:d};
d.papa=a;
var b = serializeObjet(a);
assert.equal( b, "{0:{b:25,c:6,enfant:'tab[1]'},1:{papa:'tab[0]'}}" );
var retCaseDep = parseChaine(b)
assert.equal( retCaseDep.b, 25 );
assert.equal( retCaseDep.enfant.papa, retCaseDep );

https://github.com/bormat/serializeStringifyParseCyclicObject

Edit: I have transform my script for NPM https://github.com/bormat/borto_circular_serialize and I have change function names from french to english.

answered Mar 29, 2015 at 15:44

2 Comments

This example doesn't fit the Gist. The Gist has errors.
Nice idea - but once make it ready :-) If you would make it distributed in npm, maybe you would develop even typings for that, it became probably quite popular.
2

the nodejs module serialijse provides a nice way to deal with any type of JSON objects containing cycles or javascript class instances.

const { serialize, deserialize } = require("serialijse");
 var Mary = { name: "Mary", friends: [] };
 var Bob = { name: "Bob", friends: [] };
 Mary.friends.push(Bob);
 Bob.friends.push(Mary);
 var group = [ Mary, Bob];
 console.log(group);
 // testing serialization using JSON.stringify/JSON.parse
 try {
 var jstr = JSON.stringify(group);
 var jo = JSON.parse(jstr);
 console.log(jo);
 } catch (err) {
 console.log(" JSON has failed to manage object with cyclic deps");
 console.log(" and has generated the following error message", err.message);
 }
 // now testing serialization using serialijse serialize/deserialize
 var str = serialize(group);
 var so = deserialize(str);
 console.log(" However Serialijse knows to manage object with cyclic deps !");
 console.log(so);
 assert(so[0].friends[0] == so[1]); // Mary's friend is Bob

this serializer supports

  • cycle in the object definition
  • reconstruction of class's instance
  • support for Typed Array, Map, and Set
  • ability to filter properties to skip during the serialization process.
  • binary encoding of Typed Array (Float32Array etc ... ) for performance.
answered May 2, 2021 at 16:06

Comments

-1
function stringifyObject ( obj ) {
 if ( _.isArray( obj ) || !_.isObject( obj ) ) {
 return obj.toString()
 }
 var seen = [];
 return JSON.stringify(
 obj,
 function( key, val ) {
 if (val != null && typeof val == "object") {
 if ( seen.indexOf( val ) >= 0 )
 return
 seen.push( val )
 }
 return val
 }
 );
}

A precondition was missing, otherwise the integer values in array objects are truncated, i.e. [[ 08.11.2014 12:30:13, 1095 ]] 1095 gets reduced to 095.

answered Nov 8, 2014 at 11:35

1 Comment

getting RefrenceError: Can't find variable: _

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.