91

I've got a JavaScript object definition which contains a circular reference: it has a property that references the parent object.

It also has functions that I don't want to be passed through to the server. How would I serialize and deserialize these objects?

I've read that the best method to do this is to use Douglas Crockford's stringify. However, I'm getting the following error in Chrome:

TypeError: Converting circular structure to JSON

The code:

function finger(xid, xparent){
    this.id = xid;
    this.xparent;
    //other attributes
}

function arm(xid, xparent){
    this.id = xid;
    this.parent = xparent;
    this.fingers = [];

    //other attributes

    this.moveArm = function() {
        //moveArm function details - not included in this testcase
        alert("moveArm Executed");
    }
}

 function person(xid, xparent, xname){
    this.id = xid;
    this.parent = xparent;
    this.name = xname
    this.arms = []

    this.createArms = function () {
        this.arms[this.arms.length] = new arm(this.id, this);
    }
}

function group(xid, xparent){
    this.id = xid;
    this.parent = xparent;
    this.people = [];
    that = this;

    this.createPerson = function () {
        this.people[this.people.length] = new person(this.people.length, this, "someName");
        //other commands
    }

    this.saveGroup = function () {
        alert(JSON.stringify(that.people));
    }
}

This is a test case that I created for this question. There are errors within this code but essentially I have objects within objects, and a reference passed to each object to show what the parent object is when the object is created. Each object also contains functions, which I don't want stringified. I just want the properties such as the Person.Name.

How do I serialize before sending to the server and deserialize it assuming that the same JSON is passed back?

Kamil Kiełczewski
  • 85,173
  • 29
  • 368
  • 345
user1012500
  • 1,547
  • 1
  • 16
  • 31
  • related: https://stackoverflow.com/questions/23117470/passing-an-object-with-circular-references-from-server-to-client-side-javascript/58810202#58810202 related: https://stackoverflow.com/questions/2010892/storing-objects-in-html5-localstorage?noredirect=1&lq=1 related: https://stackoverflow.com/questions/9382167/serializing-object-that-contains-cyclic-object-value/58810961#58810961 related: https://stackoverflow.com/questions/36716367/store-a-js-object-with-circular-structures-on-local-storage-and-obtain-circular/58823793#58823793 – mathheadinclouds Nov 12 '19 at 18:50
  • 1
    Crockford would have you use [his cycle.js](https://github.com/douglascrockford/JSON-js/blob/master/cycle.js) as the [`replacer` in your `stringify` call](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Parameters) as [@MattEvans](https://stackoverflow.com/a/43127625/1028230) points out, below. – ruffin May 13 '20 at 15:16

6 Answers6

134

Circular structure error occurs when you have a property of the object which is the object itself directly (a -> a) or indirectly (a -> b -> a).

To avoid the error message, tell JSON.stringify what to do when it encounters a circular reference. For example, if you have a person pointing to another person ("parent"), which may (or may not) point to the original person, do the following:

JSON.stringify( that.person, function( key, value) {
  if( key == 'parent') { return value.id;}
  else {return value;}
})

The second parameter to stringify is a filter function. Here it simply converts the referred object to its ID, but you are free to do whatever you like to break the circular reference.

You can test the above code with the following:

function Person( params) {
  this.id = params['id'];
  this.name = params['name']; 
  this.father = null;
  this.fingers = [];
  // etc.
}

var me = new Person({ id: 1, name: 'Luke'});
var him = new Person( { id:2, name: 'Darth Vader'});
me.father = him; 
JSON.stringify(me); // so far so good

him.father = me; // time travel assumed :-)
JSON.stringify(me); // "TypeError: Converting circular structure to JSON"
// But this should do the job:
JSON.stringify(me, function( key, value) {
  if(key == 'father') { 
    return value.id;
  } else {
    return value;
  };
});

BTW, I'd choose a different attribute name to "parent" since it is a reserved word in many languages (and in DOM). This tends to cause confusion down the road...

alexbt
  • 16,415
  • 6
  • 78
  • 87
tocker
  • 1,752
  • 1
  • 11
  • 9
16

No-lib

Use below replacer to generate json with string references (similar to json-path) to duplicate/circular referenced objects

let s = JSON.stringify(obj, refReplacer());

function refReplacer() {
  let m = new Map(), v= new Map(), init = null;

  // in TypeScript add "this: any" param to avoid compliation errors - as follows
  //    return function (this: any, field: any, value: any) {
  return function(field, value) {
    let p= m.get(this) + (Array.isArray(this) ? `[${field}]` : '.' + field); 
    let isComplex= value===Object(value)
    
    if (isComplex) m.set(value, p);  
    
    let pp = v.get(value)||'';
    let path = p.replace(/undefined\.\.?/,'');
    let val = pp ? `#REF:${pp[0]=='[' ? '$':'$.'}${pp}` : value;
    
    !init ? (init=value) : (val===init ? val="#REF:$" : 0);
    if(!pp && isComplex) v.set(value, path);
   
    return val;
  }
}




// ---------------
// TEST
// ---------------

// gen obj with duplicate references
let a = { a1: 1, a2: 2 };
let b = { b1: 3, b2: "4" };
let obj = { o1: { o2:  a  }, b, a }; // duplicate reference
a.a3 = [1,2,b];                      // circular reference
b.b3 = a;                            // circular reference


let s = JSON.stringify(obj, refReplacer(), 4);

console.log(s);

And following parser function to regenerate object from such "ref-json"

function parseRefJSON(json) {
  let objToPath = new Map();
  let pathToObj = new Map();
  let o = JSON.parse(json);
  
  let traverse = (parent, field) => {
    let obj = parent;
    let path = '#REF:$';

    if (field !== undefined) {
      obj = parent[field];
      path = objToPath.get(parent) + (Array.isArray(parent) ? `[${field}]` : `${field?'.'+field:''}`);
    }

    objToPath.set(obj, path);
    pathToObj.set(path, obj);
    
    let ref = pathToObj.get(obj);
    if (ref) parent[field] = ref;

    for (let f in obj) if (obj === Object(obj)) traverse(obj, f);
  }
  
  traverse(o);
  return o;
}



// ------------
// TEST
// ------------

let s = `{
    "o1": {
        "o2": {
            "a1": 1,
            "a2": 2,
            "a3": [
                1,
                2,
                {
                    "b1": 3,
                    "b2": "4",
                    "b3": "#REF:$.o1.o2"
                }
            ]
        }
    },
    "b": "#REF:$.o1.o2.a3[2]",
    "a": "#REF:$.o1.o2"
}`;

console.log('Open Chrome console to see nested fields:');
let obj = parseRefJSON(s);

console.log(obj);
Kamil Kiełczewski
  • 85,173
  • 29
  • 368
  • 345
10

It appears that dojo can represent circular references in JSON in the form : {"id":"1","me":{"$ref":"1"}}

Here is an example:

http://jsfiddle.net/dumeG/

require(["dojox/json/ref"], function(){
    var me = {
        name:"Kris",
        father:{name:"Bill"},
        mother:{name:"Karen"}
    };
    me.father.wife = me.mother;
    var jsonMe = dojox.json.ref.toJson(me); // serialize me
    alert(jsonMe);
});​

Produces:

{
   "name":"Kris",
   "father":{
     "name":"Bill",
     "wife":{
          "name":"Karen"
      }
   },
   "mother":{
     "$ref":"#father.wife"
   }
}

Note: You can also de-serialize these circular referenced objects using the dojox.json.ref.fromJson method.

Other Resources:

How to serialize DOM node to JSON even if there are circular references?

JSON.stringify can't represent circular references

Community
  • 1
  • 1
Brandon Boone
  • 16,281
  • 4
  • 73
  • 100
  • Hi, thanks for your answer. I should have stated that I'm using jquery as my is library. I didn't think it was relevant at the time.ill update my post. – user1012500 May 01 '12 at 03:25
  • @user1012500 - Dojo works fine alongside jQuery. I often include other libraries or frameworks to compensate for deficiencies in my primary framework. You may even be able to extract the `toJson` and `fromJson` methods and create your own jQuery wrapper around them. That way you won't need to pull in the whole framework. Unfortunately, jQuery does not have this functionality out of the box, and JSON.stringify can't handle these types of objects. So other than the above examples, you may have to code this functionality yourself. – Brandon Boone May 01 '12 at 11:44
  • 1
    Hi Brandon, I'm hesitant to add another library to solve the problem since it adds another footprint to the site. However, i gave dojo a shot and attempted to use your example against mine. However, i ran into the circular reference issue (I don't have much knowledge of dojo, so I've just attempted a couple of things but mainly based on your example): http://jsfiddle.net/Af3d6/1/ – user1012500 May 01 '12 at 22:00
  • 1
    [JSON](http://www.json.org/) not to be confused with [JavaScript object literals](http://www.dyn-web.com/tutorials/obj_lit.php) cannot contain functions since it is a data interchange format (like XML). So in order to serialize to the JSON format, you need a compliant object literal. [This example](http://jsfiddle.net/Af3d6/3/) modifies your example to be compliant (though it may not be what your looking for since you're no longer working with object instances). Even when you remove your toJSON functions, the references to the underlying prototype functions still makes it invalid. – Brandon Boone May 02 '12 at 01:22
  • Hi, In the end I had to go it alone and customise my stringify calls. thanks anyway. – user1012500 May 27 '12 at 10:25
  • @user1012500 - Hey no problem. Glad you were able to find a solution. For the benefit of others, you should post your own answer and mark it as the correct answer to close out this question. – Brandon Boone May 29 '12 at 02:37
  • 1
    Dojo's ref module managed to stringify / parse my models like a breeze. In my scenario the cycle.js library from crockford wasn't successful because I'm using newtonsoft json.net to serialize C# objects and it doesn't use jsonpath (as cycle.js uses). Looks like the good old dojo saved my day! Thanks for this answer. – JoaoPauloPaschoal Feb 27 '19 at 17:53
5

I found two suitable modules to handle circular references in JSON.

  1. CircularJSON https://github.com/WebReflection/circular-json whose output can be used as input to .parse(). It also works in Browsers & Node.js Also see: http://webreflection.blogspot.com.au/2013/03/solving-cycles-recursions-and-circulars.html
  2. Isaacs json-stringify-safe https://github.com/isaacs/json-stringify-safe which maybe more readable but can't be used for .parse and is only available for Node.js

Either of these should meet your needs.

nevf
  • 4,596
  • 6
  • 31
  • 32
5

Happened upon this thread because I needed to log complex objects to a page, since remote debugging wasn't possible in my particular situation. Found Douglas Crockford's (inceptor of JSON) own cycle.js, which annotates circular references as strings such that they can be reconnected after parsing. The de-cycled deep copy is safe to pass through JSON.stringify. Enjoy!

https://github.com/douglascrockford/JSON-js

cycle.js: This file contains two functions, JSON.decycle and JSON.retrocycle, which make it possible to encode cyclical structures and dags in JSON, and to then recover them. This is a capability that is not provided by ES5. JSONPath is used to represent the links.

Matt Evans
  • 51
  • 1
  • 2
-13

I used the following to eliminate the circular references:

JS.dropClasses = function(o) {

    for (var p in o) {
        if (o[p] instanceof jQuery || o[p] instanceof HTMLElement) {
            o[p] = null;
        }    
        else if (typeof o[p] == 'object' )
            JS.dropClasses(o[p]);
    }
};

JSON.stringify(JS.dropClasses(e));
Timmerz
  • 6,090
  • 5
  • 36
  • 49
  • 4
    This will remove instances of `jQuery` and `HTMLElement`, not circular references? – ZachB Jun 01 '16 at 21:33
  • 1
    @ZachB i think in his setup these are circular for him... but the problem is that just because javascript is the language in use doesn't mean we have jquery or even HTML elements. – moeiscool May 01 '17 at 18:11