Stringify (convert to JSON) a JavaScript object with circular reference
Asked Answered
C

6

92

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?

Cerebration answered 1/5, 2012 at 0:51 Comment(2)
related: #23117970 related: #2011392 related: #9382667 related: #36716867Czardas
Crockford would have you use his cycle.js as the replacer in your stringify call as @MattEvans points out, below.Wimble
D
136

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

Degas answered 30/9, 2012 at 7:14 Comment(2)
What should we change the variable parent with?Laboratory
@Laboratory owner is usually used instead of parent.Instep
I
20

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);
Inutility answered 12/5, 2020 at 10:48 Comment(1)
it's awesome....Landlordism
C
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

Chil answered 1/5, 2012 at 3:7 Comment(7)
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.Cerebration
@Cerebration - 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.Chil
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): jsfiddle.net/Af3d6/1Cerebration
JSON not to be confused with JavaScript object literals 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 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.Chil
Hi, In the end I had to go it alone and customise my stringify calls. thanks anyway.Cerebration
@Cerebration - 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.Chil
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.Lapidate
R
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.

Ribaldry answered 22/7, 2013 at 4:46 Comment(0)
L
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.

Leroi answered 30/3, 2017 at 20:20 Comment(0)
A
-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));
Alphanumeric answered 28/2, 2014 at 19:41 Comment(2)
This will remove instances of jQuery and HTMLElement, not circular references?Steerageway
@Steerageway 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.Romaineromains

© 2022 - 2024 — McMap. All rights reserved.