Skip to content

Instantly share code, notes, and snippets.

@getify
Created August 16, 2012 21:25
  • Star 9 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save getify/3373779 to your computer and use it in GitHub Desktop.
object JSON serialization that's circular-ref safe
// all this `toJSON()` does is filter out any circular refs. all other values/refs,
// it passes through untouched, so it should be totally safe. see the test examples.
// only extend the prototype if `toJSON` isn't yet defined
if (!Object.prototype.toJSON) {
Object.prototype.toJSON = function() {
function findCircularRef(obj) {
for (var i=0; i<refs.length; i++) {
if (refs[i] === obj) return true;
}
return false;
}
function traverse(obj) {
function element(el) {
if (typeof el === "object") {
if (el !== null) {
if (el instanceof Date || el instanceof Number || el instanceof Boolean || el instanceof String || el instanceof RegExp) {
return el;
}
else if (!findCircularRef(el)) {
return traverse(el);
}
}
return null;
}
return el;
}
var idx, tmp, tmp2;
if (Object.prototype.toString.call(obj) === "[object Array]") {
refs.push(obj);
tmp = [];
for (idx=0; idx<obj.length; idx++) {
tmp.push(element(obj[idx]));
}
refs.pop();
return tmp;
}
else if (typeof obj === "object") {
if (obj !== null) {
if (obj instanceof Date || obj instanceof Number || obj instanceof Boolean || obj instanceof String || obj instanceof RegExp) {
return obj;
}
else if (!findCircularRef(obj)) {
refs.push(obj);
tmp = {};
for (idx in obj) { if (obj.hasOwnProperty(idx)) {
tmp2 = element(obj[idx]);
if (tmp2 !== null) tmp[idx] = tmp2;
}}
refs.pop();
return tmp;
}
}
return null;
}
else return obj;
}
var refs = [], ret;
ret = traverse(this);
refs = [];
return ret;
};
// ES5-only: prevent this `toJSON()` from showing up in for-in loops
if (Object.defineProperty) {
Object.defineProperty(Object.prototype,"toJSON",{enumerable:false});
}
}
var a = {
b: 12,
c: true,
d: "foobar",
e: {
f: function() { alert("blah"); }, // functions get ignored
g: new Date(), // dates get their own `toJSON()` serialization called
h: [ true,1.3,"haha" ]
},
k: {},
l: /foobar/g // regexes get turned into an empty {}
};
a.i = a; // circular ref!!
a.e.i = a; // circular ref!!
a.e.j = a.e; // circular ref!!
a.e.h.push(a.e); // circular ref!! since it's in array, will be replaced with `null`
a.k.m = a.e; // **NOT** a circular ref, just a dupe ref, so leave it alone!!
// Look Ma! No circular refs!
JSON.stringify(a); // {"b":12,"c":true,"d":"foobar","e":{"g":"2012-08-17T12:11:05.647Z","h":[true,1.3,"haha",null]},"k":{"m":{"g":"2012-08-17T12:11:05.647Z","h":[true,1.3,"haha",null]}},"l":{}}
@getify
Copy link
Author

getify commented Aug 16, 2012

still a big work in progress.

  1. need to handle " and \ escaping in string values.
  2. if a in the example is made a primitive, a.toJSON() casts it to its object first (correct but weird) but then doesn't properly handle the value.

@getify
Copy link
Author

getify commented Aug 16, 2012

ok, i think #2 works fine now... works as you'd expect for the primitive types (with auto-casting) as well as the corresponding proper objects for the primitives (String, Boolean, etc).

@getify
Copy link
Author

getify commented Aug 17, 2012

OK, big rewrite, because I found out that toJSON() wasn't supposed to return a string, but a value that will be suitable for inclusion as a JSON property value: https://github.com/douglascrockford/JSON-js/blob/master/json2.js#L38 (thanks to @polotek for the link)

Now that JSON.stringify() is doing the serialization and not toJSON(), all `toJSON() needs to do is filter out the circular refs.

The final test is currently broken, so need to redo how circular refs are tested.

@getify
Copy link
Author

getify commented Aug 17, 2012

ok, circular ref checking is fixed now i think, and the above examples all work.

@nfroidure
Copy link

Replace findCircularRef(obj) by refs.indexOf(obj)!==false

@nfroidure
Copy link

Ooops
"findCircularRef(obj) by refs.indexOf(obj)>=0"
wrote to quickly

@nfroidure
Copy link

Ooops
"findCircularRef(obj) by refs.indexOf(obj)>0"
wrote to quickly

@getify
Copy link
Author

getify commented Aug 17, 2012

@nfroidure

It would be != -1... but Array#indexOf is a "recent edition" to JS, according to: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/indexOf Note the compat table which says IE9, meaning my code would break in IE<=8. That's not a good tradeoff to save a few LoC.

@getify
Copy link
Author

getify commented Aug 18, 2012

I also want to address... why would you want to have a structure with circular refs in it at all, and think that you could JSON serialize it? Is(n't) that just an unreasonable, invalid task?

I don't think it's so unreasonable. I will share two concrete use-cases for the behavior I proposed:

  1. I regularly want to do console.log(JSON.stringify(obj)) during debugging. Why? Well, for one, Firefox doesn't give you any kind of a useful output from console.log(obj) in their built-in console. And in the webkit/chrome console, that call is "dangerous" in that it's not snapshoting the object at the time of call, but making a live ref to that object, so if you have code that later (after the console.log call) changes the object, those changes frustratingly are reflected in the console.

    No, console.dir() doesn't solve this problem. JSON.stringify() is a simple way to output a serialization of an object as a snapshot, so you compare two states of an object side-by-side (no, watch and breakpoint-style debugging doesn't give you that ability).

    But, JSON.stringify(obj) is "unsafe" if the object might have circular refs. It will simply fail with a JSON error. Not very graceful at all. JSON.stringify() doesn't have a flag you can pass to it to tell it to ignore those circular refs. So you're just sunk.

  2. It's pretty common that JS apps keep data structures (by way of objects) in the browser, and regularly send all or part of that data over the wire (as JSON) to server to update the model/database persistence layer.

    It's also, I've found, sometimes quite helpful to create helpful links between objects that let you jump from one object to another it's related to, and back again, for various UI tasks (like parent-to-child and child-to-parent, etc). These "connections" between objects are just secondary helper refs in the client side... those relationships already exist likely in some other format in the back-end persistence layer, via the RDBMS, etc. So those refs don't need to make the "trip" from client to server; they're only needed on the client.

    It is quite useful if I can maintain a "ref-linked" data structure in the client that I can jump around as needed, but then take that same structure and serialize down the data to send over the wire as JSON. This is hugely helpful if the JSON serialization can just ignore all those circular refs.

@mohsen1
Copy link

mohsen1 commented Aug 18, 2012

If this works then we would be able to pass window or DOM objects to a Web Worker. That would be awesome!

@getify
Copy link
Author

getify commented Aug 18, 2012

@mohsen1 -- in theory, you could certainly do JSON.stringify(window), but I just tried it and it gave me a stack overflow error (because my approach uses recursion). The same would probably be true of a DOM object (if it has a bunch of children, especially).

But, moreover, even if it DID work, there's a bunch of stuff you would lose in the transmission. All the functions built onto the window object for instance would not be transferred. Variables, properties, and other data would make the trip, yes, but what you'd get on the other side is only a shadow of the original window... definitely not a copy of it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment