191

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
  • 48,608
  • 13
  • 72
  • 95
Loic Duros
  • 5,472
  • 10
  • 43
  • 56
  • 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. – Bojangles Feb 21 '12 at 17:32
  • 1
    are you able to add some prefix to those properties which are internal references? – wheresrhys Feb 21 '12 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. – Jeremy May 24 '13 at 14:23
  • Late to the party but there is a [github](http://stackoverflow.com/a/23961876/2464634) project to handle this. – Preston S May 30 '14 at 19:36
  • 4
    Possible duplicate of [JSON.stringify, avoid TypeError: Converting circular structure to JSON](http://stackoverflow.com/questions/11616630/json-stringify-avoid-typeerror-converting-circular-structure-to-json) – user3791372 Feb 12 '16 at 08: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. – Tigerware Sep 06 '19 at 11:39
  • @BluE I agree. I found a great alternative! https://stackoverflow.com/a/62839421/1599699 – Andrew Jul 10 '20 at 17:26

8 Answers8

279

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)))
georg
  • 211,518
  • 52
  • 313
  • 390
  • 4
    aaah nice! Thanks, I'm going to try this. I found a solution created by Douglas Crockford (https://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! – Loic Duros Feb 21 '12 at 17:44
  • 3
    @LoicDuros The license is "public domain". Meaning, you can do anything you want with it. – Ates Goral Oct 14 '12 at 14:35
  • 2
    this code produces cycling loops, beware of using, very potential crashes your app. needs correct semicolons and is not useable on event objects! – Ol Sen Apr 22 '13 at 16:07
  • i have seen this while checking with textmate and automatic script reloading in a webview. very sure it began to loop, then testet in your fiddle it worked nice. so i came to conclusion its made by missing semicolons and on top fiddle has errorhandling for itself. – Ol Sen Apr 22 '13 at 18:16
  • 3
    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 – Gio Apr 04 '14 at 14:37
  • 1
    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. – HoldOffHunger Apr 19 '18 at 17:38
  • There is an article on this issue from Mozilla dev docs: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value The article has another example for getCircularReplacer function (second argument in `stringify` from this answer – Valera Tumash May 18 '23 at 06:24
12

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.

Andrew
  • 5,839
  • 1
  • 51
  • 72
7

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)

mathheadinclouds
  • 3,507
  • 2
  • 27
  • 37
  • 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? – gignu Oct 01 '21 at 12:06
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__"]}]}
Ol Sen
  • 3,163
  • 2
  • 21
  • 30
  • 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 – Ol Sen Apr 22 '13 at 18:09
  • 3
    This should be `seen.indexOf(v) != -1` –  Jul 27 '16 at 01:45
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/

Hoff
  • 243
  • 1
  • 6
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.

bormat
  • 1,309
  • 12
  • 16
  • This example doesn't fit the Gist. The Gist has errors. – Ernst Robert Feb 22 '16 at 00:33
  • 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. – peterh Feb 21 '17 at 08:36
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.
Etienne
  • 16,249
  • 3
  • 26
  • 31
-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.