Serializing object that contains cyclic object value
Asked Answered
D

8

195

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

Dressing answered 21/2, 2012 at 17:27 Comment(7)
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.Fernandefernandel
are you able to add some prefix to those properties which are internal references?Clangor
@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.Armagh
Late to the party but there is a github project to handle this.Languet
Possible duplicate of JSON.stringify, avoid TypeError: Converting circular structure to JSONKatharyn
I wish JSON would be smarter, or an easier way of solving this. The solutions are too troublesome for simple(!) debugging purposes imo.Teena
@BluE I agree. I found a great alternative! https://mcmap.net/q/36581/-serializing-object-that-contains-cyclic-object-valueRhombus
I
282

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)))
Isolda answered 21/2, 2012 at 17:41 Comment(7)
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!Dressing
@LoicDuros The license is "public domain". Meaning, you can do anything you want with it.Lavellelaven
this code produces cycling loops, beware of using, very potential crashes your app. needs correct semicolons and is not useable on event objects!Aholah
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.Aholah
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 itImpale
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.Estate
There is an article on this issue from Mozilla dev docs: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… The article has another example for getCircularReplacer function (second argument in stringify from this answerFiddler
R
13

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.

Rhombus answered 10/7, 2020 at 17:26 Comment(0)
G
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)

Glove answered 12/11, 2019 at 2:4 Comment(1)
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?Otology
A
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__"]}]}
Aholah answered 22/4, 2013 at 18:5 Comment(2)
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 hereAholah
This should be seen.indexOf(v) != -1Chastain
T
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/

Tibbitts answered 28/3, 2014 at 21:31 Comment(0)
H
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.

Haff answered 29/3, 2015 at 15:44 Comment(2)
This example doesn't fit the Gist. The Gist has errors.Salinasalinas
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.Ironmaster
W
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.
Wivern answered 2/5, 2021 at 16:6 Comment(0)
B
-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.

Briannebriano answered 8/11, 2014 at 11:35 Comment(1)
getting RefrenceError: Can't find variable: _Sapir

© 2022 - 2024 — McMap. All rights reserved.