How can I print a circular structure in a JSON-like format?
Asked Answered
P

32

888

I have a big object I want to convert to JSON and send. However it has circular structure, so if I try to use JSON.stringify() I'll get:

TypeError: Converting circular structure to JSON

or

TypeError: cyclic object value

I want to toss whatever circular references exist and send whatever can be stringified. How do I do that?

Thanks.

var obj = {
  a: "foo",
  b: obj
}

I want to stringify obj into:

{"a":"foo"}
Pejsach answered 23/7, 2012 at 16:30 Comment(5)
Could you please post a sample object with a circular reference that you'd like to parse ?Wileywilfong
something like this?Dimaggio
possible duplicate of serializing object that contains cyclic object valueBid
Late to the party but there is a github project to handle this.Robbegrillet
closely related question: #23117970Arana
R
758

Use JSON.stringify with a custom replacer. For example:

// Demo: Circular reference
var circ = {};
circ.circ = circ;

// Note: cache should not be re-used by repeated calls to JSON.stringify.
var cache = [];
JSON.stringify(circ, (key, value) => {
  if (typeof value === 'object' && value !== null) {
    // Duplicate reference found, discard key
    if (cache.includes(value)) return;

    // Store value in our collection
    cache.push(value);
  }
  return value;
});
cache = null; // Enable garbage collection

The replacer in this example is not 100% correct (depending on your definition of "duplicate"). In the following case, a value is discarded:

var a = {b:1}
var o = {};
o.one = a;
o.two = a;
// one and two point to the same object, but two is discarded:
JSON.stringify(o, ...);

But the concept stands: Use a custom replacer, and keep track of the parsed object values.

As a utility function written in es6:

// safely handles circular references
JSON.safeStringify = (obj, indent = 2) => {
  let cache = [];
  const retVal = JSON.stringify(
    obj,
    (key, value) =>
      typeof value === "object" && value !== null
        ? cache.includes(value)
          ? undefined // Duplicate reference found, discard key
          : cache.push(value) && value // Store value in our collection
        : value,
    indent
  );
  cache = null;
  return retVal;
};

// Example:
console.log('options', JSON.safeStringify(options))
Ramsay answered 23/7, 2012 at 16:56 Comment(28)
There was a small bug in your code but the overall idea I understand, thanks!Pejsach
You should be checking typeof value not typeof cachePejsach
By the way - I ran into an interesting scenario. Someone printed the attrs parameters in an AngularJS directive. I think this object is just too big to be printed. Chrome gives up after 2582 keys. So I added to my solution a cache length limit.Tymon
JSON.stringify(window, ...) on this page breaks with the exception: InvalidStateError: Failed to read the 'selectionDirection' property from 'HTMLInputElement': The input element's type ('hidden') does not support selection.Auxiliaries
@Auxiliaries Trying to serialize a non-serializable object won't result in anything useful... What would you expect from serializing window?Ramsay
I'm getting the same error trying to stringify an instance of CKEditor. It saves inside the DOM element associated, so, I can't stringify with this approachPotence
@CruzDiablo Serializing DOM is usually meaningless. However, if you can think of a meaningful serialization method for your purposes, then you could try to add a custom serialized to DOM objects: Node.prototype.toJSON = function() { return 'whatever you think that is right'; }; (if you want anything more generic/specific, just try anything in the prototype tree: HTMLDivElement implements HTMLElement implements Element implements Node implements EventTarget; note: this may be browser-dependent, the previous tree is true for Chrome)Ramsay
this is wrong because it will skip the second appearance of objects that are contained twice, even if not in a really cyclic structure. var a={id:1}; JSON.stringify([a,a]);Ingleside
@Ingleside "The replacer in this example is not 100% correct (depending on your definition of "duplicate"). But the concept stands: Use a custom replacer, and keep track of the parsed object values."Ramsay
Don't use this indeed. It strips out every duplicate object (which isn't a circular dependency) and is hellishly slow by checking every seen object.Sacrificial
The GC concern here is arguably redundant. If this is run as a single script then script immediately terminates. If this is encapsulated inside a function for implementation then cache will be unreachable developer.mozilla.org/en-US/docs/Web/JavaScript/…Asiatic
Note that IE 8 does not support Array.prototype.indexOf(). You may need to polyfill. See developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…Octennial
@AndriMöll Pretty sure Rob's just concisely demonstrating custom replacers, which he's done very well. Preston's comment points to some Crockfordian code that is live-fire robust, if that's what you're after. But the concept is the same.Mckamey
@ruffin: Since making that comment, I've taken the JSON Stringify Safe lib from Isaac and rewrote it: github.com/isaacs/json-stringify-safe. I'm not sure what the supposedly live-fire robust Crockford code does. It looks overly complicated and seems to do the same poor linear check that I warned against above.Sacrificial
@AndriMöll using your stringifier and passing in window I still get Error: Permission denied to access property "toJSON" , it's funny cause it works with any other cyclic test obj but not window or 'document! could there be that there is an intentional limitation placed on the JSON.stringify` that inhibits it from executing certain objects?Nell
@SabaAhang: Presumably one of the global objects you're stringifying has a toJSON that throws. FYI: By passing in window you'll be stringifying every global variable. Probably not a good idea.Sacrificial
@ruffin: The linear check is worthy of mocking no matter who writes it. ;-) See the code related to the objects array and the comment above it: «This is a hard way, linear search that will get slower as the number of unique objects grows.»Sacrificial
I was using this function without flushing the cache, and this gave very unexpected results. Saving the next guy some headache, i took the liberty to make the cache var local.Poultice
This is correct, but there is a better solution using Map! We can make this more performant using a hashmap, see: https://mcmap.net/q/36423/-how-can-i-print-a-circular-structure-in-a-json-like-formatHemialgia
Small addition helped me reduce useless for debug-purpose info: if( key.startsWith('_') ) { return; }Sherylsheryle
@WebBrother I know exactly what your problem is, I have known about this situation for years, I just created a fix for this, please try it out: github.com/ORESoftware/safe-stringify, look for stringifyDeep, try it on your object and see if t works for you.Hemialgia
@RobW this solution is improved with the try/catch block you added recently, but I found another limitation, see this gist please: gist.github.com/ORESoftware/10bd74e27728a2aa764df4d6c6ecada8Hemialgia
Isn't using indexOf for each element awfully inefficient (like exponentially)? Wouldn't it be wieser to use var cache = new Set() and cache.has(value) instead?Hast
in Arrays it just works fine for the first element then all elements become nullPlating
Note: an edit to this question by @funrob in May 2018 fixed the issue with some items being discarded; I reverted the edit as it changed the substance of the answer (and made the part of the answer that referenced the issue nonsensical, as well as some of the comments), but the fix is to replace the return; statement with try { return JSON.parse(JSON.stringify(value)); } catch (error) { return; }Turtleneck
This doesnt work if. obj1 = {}; obj2 = {foo:obj, bar:obj} The stringify removes bar because it exists in cache but it is not cyclic.Witticism
magnificent answer, this is terse and helped me as well.Forge
@AndriMöll Over five years later, I notice that I stand corrected. ;^) Your "circular stringify" solution does seem much better than Crockford's in exactly the ways you describe. He's a smart dude, so I'm worried there's something I've missed, but until (and if!) then, a sincere thanks.Mckamey
Y
912

In Node.js, you can use util.inspect(object). It automatically replaces circular links with "[Circular]".


Albeit being built-in (no installation is required), you must import it

import * as util from 'util' // has no default export
import { inspect } from 'util' // or directly
// or 
var util = require('util')

To use it, simply call

console.log(util.inspect(myObject))

Also be aware that you can pass options object to inspect (see link above)

inspect(myObject[, options: {showHidden, depth, colors, showProxy, ...moreOptions}])


Please, read and give kudos to commenters below...

Yellows answered 21/8, 2013 at 9:54 Comment(18)
util is a built-in module, you do not have to install it.Flea
console.log(util.inspect(obj))Lemmy
This was a great suggestion! Thanks so much! I never thought seeing a console logged object would be so satisfying, but it was as if you broke all the objs open for me now to work with.Thicken
Note that if for some reason you need to use JSON.stringify, you CANNOT USE util.inspect, because it outputs a string, and not an object.Webfoot
One more tip about this: I use util.inspect with the following arguments, console.log(util.inspect(object, false, 10, true)); the second argument is for setting showHiddenProperties to false, the 10 is to tell it to dig into the object up to 10 levels deep and the last 'true' is to ask it to colorize the output. reference: docs.nodejitsu.com/articles/getting-started/…Embassy
@Flea it is built-in, but you still have to load the module var util = require('util');Paving
Don't be a dunce like me, its just obj_str = util.inspect(thing), NOT <s>garbage_str = JSON.stringify(util.inspect(thing))</s>Centreboard
Do note that util.inspect does not always return valid JSON. For example util.inspect(new Error('This is an example error'))Brain
This is much better than mucking around with checking types. Why can't stringify just work like this? If it knows there's a circular reference, why can't it just be told to ignore it???Wobbling
Albeit being built-in, you must import it with either import * as util from 'util' or var util = require('util'). To use it, simply call console.log(util.inspect(myObject)). Also be aware that you can pass additional parameters to inspect(myObject[, options: {showHidden, depth, colors, showProxy, ...otherOptions}]).Kalimantan
OMG it works! cb(require('util').inspect(sequelize)) thank you so much!Brettbretz
I imported this into my codebase and it added almost a thousand lines of codeParishioner
Just wanted point out that util.insect() will NOT produce valid Json string... so it is kind of different from JSON.stringify()Mandalay
util.inspect() didn't work for me. It introduces some return characters which could be removed with string.replace(). So I just converted it to new object with Object.assign() (it happened with firebase firestore data)Tintype
This was very helpful thank you! Here is an example with the options that prints everything, in case you are debugging a large object. let inspectOptions = {showHidden: false, depth:null, maxArrayLength: null }; let returnVal = util.inspect(thing,inspectOptions);//DebugEarl
You don't need the 2nd import, since you are referring directly to inspect using util.inspect().Newspaper
Note: util.inspect will only indicate [Circular] if the "depth" property is large enough to contain the entire cycle; hence a = {}; a.b = {}; a.b.c = {}; a.b.c.d = {}; a.b.c.d.e = a; util.inspect(a) will print { b: { c: { d: [Object] } } } despite the circular reference, but util.inspect(a, {depth: 3}) will display [Circular]Accustomed
@Centreboard not sure what you mean by that. These docs say you can do console.log(inspect(obj): nodejs.org/en/knowledge/getting-started/how-to-use-util-inspectHand
R
758

Use JSON.stringify with a custom replacer. For example:

// Demo: Circular reference
var circ = {};
circ.circ = circ;

// Note: cache should not be re-used by repeated calls to JSON.stringify.
var cache = [];
JSON.stringify(circ, (key, value) => {
  if (typeof value === 'object' && value !== null) {
    // Duplicate reference found, discard key
    if (cache.includes(value)) return;

    // Store value in our collection
    cache.push(value);
  }
  return value;
});
cache = null; // Enable garbage collection

The replacer in this example is not 100% correct (depending on your definition of "duplicate"). In the following case, a value is discarded:

var a = {b:1}
var o = {};
o.one = a;
o.two = a;
// one and two point to the same object, but two is discarded:
JSON.stringify(o, ...);

But the concept stands: Use a custom replacer, and keep track of the parsed object values.

As a utility function written in es6:

// safely handles circular references
JSON.safeStringify = (obj, indent = 2) => {
  let cache = [];
  const retVal = JSON.stringify(
    obj,
    (key, value) =>
      typeof value === "object" && value !== null
        ? cache.includes(value)
          ? undefined // Duplicate reference found, discard key
          : cache.push(value) && value // Store value in our collection
        : value,
    indent
  );
  cache = null;
  return retVal;
};

// Example:
console.log('options', JSON.safeStringify(options))
Ramsay answered 23/7, 2012 at 16:56 Comment(28)
There was a small bug in your code but the overall idea I understand, thanks!Pejsach
You should be checking typeof value not typeof cachePejsach
By the way - I ran into an interesting scenario. Someone printed the attrs parameters in an AngularJS directive. I think this object is just too big to be printed. Chrome gives up after 2582 keys. So I added to my solution a cache length limit.Tymon
JSON.stringify(window, ...) on this page breaks with the exception: InvalidStateError: Failed to read the 'selectionDirection' property from 'HTMLInputElement': The input element's type ('hidden') does not support selection.Auxiliaries
@Auxiliaries Trying to serialize a non-serializable object won't result in anything useful... What would you expect from serializing window?Ramsay
I'm getting the same error trying to stringify an instance of CKEditor. It saves inside the DOM element associated, so, I can't stringify with this approachPotence
@CruzDiablo Serializing DOM is usually meaningless. However, if you can think of a meaningful serialization method for your purposes, then you could try to add a custom serialized to DOM objects: Node.prototype.toJSON = function() { return 'whatever you think that is right'; }; (if you want anything more generic/specific, just try anything in the prototype tree: HTMLDivElement implements HTMLElement implements Element implements Node implements EventTarget; note: this may be browser-dependent, the previous tree is true for Chrome)Ramsay
this is wrong because it will skip the second appearance of objects that are contained twice, even if not in a really cyclic structure. var a={id:1}; JSON.stringify([a,a]);Ingleside
@Ingleside "The replacer in this example is not 100% correct (depending on your definition of "duplicate"). But the concept stands: Use a custom replacer, and keep track of the parsed object values."Ramsay
Don't use this indeed. It strips out every duplicate object (which isn't a circular dependency) and is hellishly slow by checking every seen object.Sacrificial
The GC concern here is arguably redundant. If this is run as a single script then script immediately terminates. If this is encapsulated inside a function for implementation then cache will be unreachable developer.mozilla.org/en-US/docs/Web/JavaScript/…Asiatic
Note that IE 8 does not support Array.prototype.indexOf(). You may need to polyfill. See developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…Octennial
@AndriMöll Pretty sure Rob's just concisely demonstrating custom replacers, which he's done very well. Preston's comment points to some Crockfordian code that is live-fire robust, if that's what you're after. But the concept is the same.Mckamey
@ruffin: Since making that comment, I've taken the JSON Stringify Safe lib from Isaac and rewrote it: github.com/isaacs/json-stringify-safe. I'm not sure what the supposedly live-fire robust Crockford code does. It looks overly complicated and seems to do the same poor linear check that I warned against above.Sacrificial
@AndriMöll using your stringifier and passing in window I still get Error: Permission denied to access property "toJSON" , it's funny cause it works with any other cyclic test obj but not window or 'document! could there be that there is an intentional limitation placed on the JSON.stringify` that inhibits it from executing certain objects?Nell
@SabaAhang: Presumably one of the global objects you're stringifying has a toJSON that throws. FYI: By passing in window you'll be stringifying every global variable. Probably not a good idea.Sacrificial
@ruffin: The linear check is worthy of mocking no matter who writes it. ;-) See the code related to the objects array and the comment above it: «This is a hard way, linear search that will get slower as the number of unique objects grows.»Sacrificial
I was using this function without flushing the cache, and this gave very unexpected results. Saving the next guy some headache, i took the liberty to make the cache var local.Poultice
This is correct, but there is a better solution using Map! We can make this more performant using a hashmap, see: https://mcmap.net/q/36423/-how-can-i-print-a-circular-structure-in-a-json-like-formatHemialgia
Small addition helped me reduce useless for debug-purpose info: if( key.startsWith('_') ) { return; }Sherylsheryle
@WebBrother I know exactly what your problem is, I have known about this situation for years, I just created a fix for this, please try it out: github.com/ORESoftware/safe-stringify, look for stringifyDeep, try it on your object and see if t works for you.Hemialgia
@RobW this solution is improved with the try/catch block you added recently, but I found another limitation, see this gist please: gist.github.com/ORESoftware/10bd74e27728a2aa764df4d6c6ecada8Hemialgia
Isn't using indexOf for each element awfully inefficient (like exponentially)? Wouldn't it be wieser to use var cache = new Set() and cache.has(value) instead?Hast
in Arrays it just works fine for the first element then all elements become nullPlating
Note: an edit to this question by @funrob in May 2018 fixed the issue with some items being discarded; I reverted the edit as it changed the substance of the answer (and made the part of the answer that referenced the issue nonsensical, as well as some of the comments), but the fix is to replace the return; statement with try { return JSON.parse(JSON.stringify(value)); } catch (error) { return; }Turtleneck
This doesnt work if. obj1 = {}; obj2 = {foo:obj, bar:obj} The stringify removes bar because it exists in cache but it is not cyclic.Witticism
magnificent answer, this is terse and helped me as well.Forge
@AndriMöll Over five years later, I notice that I stand corrected. ;^) Your "circular stringify" solution does seem much better than Crockford's in exactly the ways you describe. He's a smart dude, so I'm worried there's something I've missed, but until (and if!) then, a sincere thanks.Mckamey
H
278

I wonder why nobody posted the proper solution from MDN page yet...

const circularReference = {otherData: 123};
circularReference.myself = circularReference;

const getCircularReplacer = () => {
  const seen = new WeakSet();
  return (key, value) => {
    if (typeof value === "object" && value !== null) {
      if (seen.has(value)) {
        return;
      }
      seen.add(value);
    }
    return value;
  };
};

const stringified = JSON.stringify(circularReference, getCircularReplacer());

console.log(stringified);

Seen values should be stored in a set, not in array (replacer gets called on every element). Using an array would result in quadratic O((N^2 + N)/2) complexity instead of linear O(N)!

Note, like in the accepted answer, this solution removes all repeating values, not just the circular ones.

Hast answered 11/12, 2018 at 19:34 Comment(8)
Neat, but this is ES2015 only. No IE support.Abstraction
Yoda says: "If still supporting IE one is, then use a transpiler one should."Longfellow
replacer = () => { const seen = new WeakSet(); return (key, value) => { if (typeof value === "object" && value !== null) { if (seen.has(value)) { return; } seen.add(value); } return value; }; } () => { const seen = new WeakSet(); return (key, value) => { if (typeof value === "object" && value !== null) { if (seen.has(value)) { return; } seen.add(value); … JSON.stringify({a:1, b: '2'}, replacer) returns undefined in chromeAnnieannihilate
I got error stack.get is not a function with express' Response object. decycle from github.com/douglascrockford/JSON-js/blob/master/cycle.js worked.Biocatalyst
This answer (like the accepted answer) has a bug (that is commented here by user2451227), when o = {}; JSON.stringify([o, o], getCircularReplacer()).Oppose
@robertotomás, You should invoke replacer() and pass its return-value (that is another function, that is the actual replacer) to JSON.stringify(). Not your replacer itself! Mean: JSON.stringify({a:1, b: '2'}, replacer()). Name it getReplacer for clarity!Oppose
I get Uncaught DOMException: Blocked a frame with origin "http://mywebsite.local" from accessing a cross-origin frame.Periapt
This detects duplicate objects in the structure, not circular. While a circular will contain a duplicate reference, not all duplicates are circular objects. So, this removes non-circular references too. For example with this, const a = {greeting: "hi"}; const obj = {first: a, second: a};, using circular() when stringifying obj will remove the second property, even though it is NOT circular at all. So, it removes unnecessary things.Territerrible
S
86

just do

npm i --save circular-json

then in your js file

const CircularJSON = require('circular-json');
...
const json = CircularJSON.stringify(obj);

https://github.com/WebReflection/circular-json

NOTE: I have nothing to do with this package. But I do use it for this.

Update 2020

Please note CircularJSON is in maintenance only and flatted is its successor.

Subdeacon answered 22/7, 2016 at 16:44 Comment(6)
Thanks a lot! Great library, saved tons of time. Super tiny (just 1.4KB minified).Towner
I think you might require some more justification for using a module than "just do". And it's not great to overwrite JSON on principle.Claudell
I needed to copy an object to use for stub testing. This answer was perfect. I copied the object and then removed the override. Thanks!!Mooch
According to the author, this package has been deprecated. CircularJSON is in maintenance only, flatted is its successor. Link: github.com/WebReflection/flatted#flattedGynecoid
Beware, the 'flatted' (and circular-json?) package doesn't replicate JSON.stringify() functionality. It creates its own non-JSON format. (e.g., Flatted.stringify({blah: 1}) results in [{"blah":1}]) I see someone tried to raise an issue about this, and the author berated them and locked the issue to comments.Tortilla
Attempt to log object, containing DOM form, caused Visual Studio Code to hang, but util.inspect works.Fda
T
49

I really liked Trindaz's solution - more verbose, however it had some bugs. I fixed them for whoever likes it too.

Plus, I added a length limit on my cache objects.

If the object I am printing is really big - I mean infinitely big - I want to limit my algorithm.

JSON.stringifyOnce = function(obj, replacer, indent){
    var printedObjects = [];
    var printedObjectKeys = [];

    function printOnceReplacer(key, value){
        if ( printedObjects.length > 2000){ // browsers will not print more than 20K, I don't see the point to allow 2K.. algorithm will not be fast anyway if we have too many objects
        return 'object too long';
        }
        var printedObjIndex = false;
        printedObjects.forEach(function(obj, index){
            if(obj===value){
                printedObjIndex = index;
            }
        });

        if ( key == ''){ //root element
             printedObjects.push(obj);
            printedObjectKeys.push("root");
             return value;
        }

        else if(printedObjIndex+"" != "false" && typeof(value)=="object"){
            if ( printedObjectKeys[printedObjIndex] == "root"){
                return "(pointer to root)";
            }else{
                return "(see " + ((!!value && !!value.constructor) ? value.constructor.name.toLowerCase()  : typeof(value)) + " with key " + printedObjectKeys[printedObjIndex] + ")";
            }
        }else{

            var qualifiedKey = key || "(empty key)";
            printedObjects.push(value);
            printedObjectKeys.push(qualifiedKey);
            if(replacer){
                return replacer(key, value);
            }else{
                return value;
            }
        }
    }
    return JSON.stringify(obj, printOnceReplacer, indent);
};
Tymon answered 21/7, 2013 at 14:47 Comment(3)
You're missing a null check on this line : return "(see " + (!!value.constructor ? value.constructor.name.toLowerCase() : typeof(value)) + " with key " + printedObjectKeys[printedObjIndex] + ")";Quiles
I will gladly add it. just let me know what is nullable as I did experience any problems so far.Tymon
// browsers will not print more than 20K - But you put limit as 2k. Perhaps change for the future?Resign
D
48

Note that there is also a JSON.decycle method implemented by Douglas Crockford. See his cycle.js. This allows you to stringify almost any standard structure:

var a = [];
a[0] = a;
a[1] = 123;
console.log(JSON.stringify(JSON.decycle(a)));
// result: '[{"$ref":"$"},123]'.

You can also recreate original object with retrocycle method. So you don't have to remove cycles from objects to stringify them.

However this will not work for DOM Nodes (which are typical cause of cycles in real life use-cases). For example this will throw:

var a = [document.body];
console.log(JSON.stringify(JSON.decycle(a)));

I've made a fork to solve that problem (see my cycle.js fork). This should work fine:

var a = [document.body];
console.log(JSON.stringify(JSON.decycle(a, true)));

Note that in my fork JSON.decycle(variable) works as in the original and will throw an exception when the variable contain DOM nodes/elements.

When you use JSON.decycle(variable, true) you accept the fact that the result will not be reversible (retrocycle will not re-create DOM nodes). DOM elements should be identifiable to some extent though. For example if a div element has an id then it will be replaced with a string "div#id-of-the-element".

Dermatosis answered 19/2, 2014 at 8:59 Comment(6)
Both his code and yours give me a "RangeError: Maximum call stack size exceeded" when I use them.Doubleteam
I can have a look if you provide your code on the Fiddle or add an issue on Github: github.com/Eccenux/JSON-js/issuesDermatosis
This is what I was looking for. JSON.decycle(a, true) what happens when you pass true as a parameter to decycle function.Carbonaceous
@Carbonaceous true makes stringifyNodes option true in the fork. This will dump e.g. div with id="some-id" to string: div#some-id. You will avoid some problems, but you won't to be able to fully retro-cycle.Dermatosis
There is npm package npmjs.com/package/json-js, but it wasn't updated for a whileFda
Crockford's version worked for me.Biocatalyst
H
42

@RobW's answer is correct, but this is more performant ! Because it uses a hashmap/set:

const customStringify = function (v) {
  const cache = new Set();
  return JSON.stringify(v, function (key, value) {
    if (typeof value === 'object' && value !== null) {
      if (cache.has(value)) {
        // Circular reference found
        try {
          // If this value does not reference a parent it can be deduped
         return JSON.parse(JSON.stringify(value));
        }
        catch (err) {
          // discard key if value cannot be deduped
         return;
        }
      }
      // Store value in our set
      cache.add(value);
    }
    return value;
  });
};
Hemialgia answered 14/1, 2018 at 22:6 Comment(5)
For deeply nested objects with circular references, try stringifyDeep => github.com/ORESoftware/safe-stringifyHemialgia
It's possibly that the Set implementation just uses an array and indexOf under the hood, but I haven't confirmed that.Hemialgia
This is removing parent nodes having child nodes even with different values - eg - {"a":{"b":{"a":"d"}}} and even removing nodes having empty object {}Foreordain
Can you show an example of that Sandip? create a gist.github.com or whatnotHemialgia
Excellent !!! First (from top, but checked 2-3 function solutions only) working solution here under node.js and Fission ;-) - libraries hanged-up.Speos
F
26

I'd recommend checking out json-stringify-safe from @isaacs-- it's used in NPM.

BTW- if you're not using Node.js, you can just copy and paste lines 4-27 from the relevant part of the source code.

To install:

$ npm install json-stringify-safe --save

To use:

// Require the thing
var stringify = require('json-stringify-safe');

// Take some nasty circular object
var theBigNasty = {
  a: "foo",
  b: theBigNasty
};

// Then clean it up a little bit
var sanitized = JSON.parse(stringify(theBigNasty));

This yields:

{
  a: 'foo',
  b: '[Circular]'
}

Note that, just like with the vanilla JSON.stringify function as @Rob W mentioned, you can also customize the sanitization behavior by passing in a "replacer" function as the second argument to stringify(). If you find yourself needing a simple example of how to do this, I just wrote a custom replacer which coerces errors, regexps, and functions into human-readable strings here.

Fiche answered 13/5, 2015 at 1:48 Comment(1)
I like this solution because it solves the problem with fewer limitations than others. It avoids: 1) only working on NodeJS, 2) removing duplicates not just cycles, 3) outputting JSON with non-standard overall structure. It's also packaged nicely on npm, but with nice and short source code (allowing simple copy-paste).Auriol
A
14

For future googlers searching for a solution to this problem when you don't know the keys of all circular references, you could use a wrapper around the JSON.stringify function to rule out circular references. See an example script at https://gist.github.com/4653128.

The solution essentially boils down to keeping a reference to previously printed objects in an array, and checking that in a replacer function before returning a value. It's more constrictive than only ruling out circular references, because it also rules out ever printing an object twice, one of the side affects of which is to avoid circular references.

Example wrapper:

function stringifyOnce(obj, replacer, indent){
    var printedObjects = [];
    var printedObjectKeys = [];

    function printOnceReplacer(key, value){
        var printedObjIndex = false;
        printedObjects.forEach(function(obj, index){
            if(obj===value){
                printedObjIndex = index;
            }
        });

        if(printedObjIndex && typeof(value)=="object"){
            return "(see " + value.constructor.name.toLowerCase() + " with key " + printedObjectKeys[printedObjIndex] + ")";
        }else{
            var qualifiedKey = key || "(empty key)";
            printedObjects.push(value);
            printedObjectKeys.push(qualifiedKey);
            if(replacer){
                return replacer(key, value);
            }else{
                return value;
            }
        }
    }
    return JSON.stringify(obj, printOnceReplacer, indent);
}
Asiatic answered 28/1, 2013 at 4:50 Comment(3)
Nice code. You have a silly error though, you write if(printedObjIndex) while you should write if(printedObjIndex==false) because index can also be 0 which is translated to false unless you explicitly state otherwise.Tymon
@guymograbi Don't you mean ===? 0 == false is true, 0 === false is false. ;^) But I'd rather not initialize printedObjIndex to false, as then you can check against undefined so that you're (well, Trindaz's) not mixing metaphors as strangely.Mckamey
@Mckamey nice catch. yes obviously, always use hard equality and jshint to catch such silly mistakes.Tymon
P
5
var a={b:"b"};
a.a=a;
JSON.stringify(preventCircularJson(a));

evaluates to:

"{"b":"b","a":"CIRCULAR_REFERENCE_REMOVED"}"

with the function:

/**
 * Traverses a javascript object, and deletes all circular values
 * @param source object to remove circular references from
 * @param censoredMessage optional: what to put instead of censored values
 * @param censorTheseItems should be kept null, used in recursion
 * @returns {undefined}
 */
function preventCircularJson(source, censoredMessage, censorTheseItems) {
    //init recursive value if this is the first call
    censorTheseItems = censorTheseItems || [source];
    //default if none is specified
    censoredMessage = censoredMessage || "CIRCULAR_REFERENCE_REMOVED";
    //values that have allready apeared will be placed here:
    var recursiveItems = {};
    //initaite a censored clone to return back
    var ret = {};
    //traverse the object:
    for (var key in source) {
        var value = source[key]
        if (typeof value == "object") {
            //re-examine all complex children again later:
            recursiveItems[key] = value;
        } else {
            //simple values copied as is
            ret[key] = value;
        }
    }
    //create list of values to censor:
    var censorChildItems = [];
    for (var key in recursiveItems) {
        var value = source[key];
        //all complex child objects should not apear again in children:
        censorChildItems.push(value);
    }
    //censor all circular values
    for (var key in recursiveItems) {
        var value = source[key];
        var censored = false;
        censorTheseItems.forEach(function (item) {
            if (item === value) {
                censored = true;
            }
        });
        if (censored) {
            //change circular values to this
            value = censoredMessage;
        } else {
            //recursion:
            value = preventCircularJson(value, censoredMessage, censorChildItems.concat(censorTheseItems));
        }
        ret[key] = value

    }

    return ret;
}
Purposeful answered 8/7, 2015 at 11:37 Comment(1)
Works, but is substantially more verbose than it could be.Auriol
O
5

This solution fixes the issue reported by user2451227 on accepted answer (when o = {}; JSON.stringify([o, o], getCircularReplacer())).

function newCircularReplacer () {
  const seenValues = []
  return circularReplacer

  function circularReplacer (key, value) {
    if (typeof value === 'object' && value !== null && Object.keys(value).length) {
      const stackSize = seenValues.length
      if (stackSize) {
        for (let n = stackSize - 1; seenValues[n][key] !== value; --n) seenValues.pop() // Clean up expired references
        if (seenValues.includes(value)) return '[Circular]'
      }
      seenValues.push(value)
    }
    return value
  }
}

let o = {a: 1}
o.b = o // Circular reference
console.log(
  JSON.stringify(o, newCircularReplacer()) // {a:1,b:[Circular]} ✅
)

o = {}
const notCircularReference = [o, o]
console.log(
  JSON.stringify(notCircularReference, newCircularReplacer()) // [{},{}] ✅ (NOT circular reference)
)
Oppose answered 1/6, 2022 at 7:29 Comment(2)
How does answering a question with a link to the accepted answer of the same question help?Robson
@AdamYost; The mentioned answer had an issue. This answer resolves it.Oppose
W
4

Use the JSON.stringify method with a replacer. Read this documentation for more information. http://msdn.microsoft.com/en-us/library/cc836459%28v=vs.94%29.aspx

var obj = {
  a: "foo",
  b: obj
}

var replacement = {"b":undefined};

alert(JSON.stringify(obj,replacement));

Figure out a way to populate the replacement array with cyclic references. You can use the typeof method to find if an the property is of type 'object' ( reference ) and an exact equality check ( === ) to verify circular reference.

Wileywilfong answered 23/7, 2012 at 17:26 Comment(2)
This might only work in IE (considering the fact that MSDN is documentation from Microsoft, and Microsoft creates IE). In Firefox/Chrome, jsfiddle.net/ppmaW generates the circular reference error. FYI: var obj = {foo:obj} does not create a circular reference. Instead, it creates an object whose foo attribute refers to the previous value of obj (undefined if not previously defined, declared because of var obj).Ramsay
Yes, this doesn't work in Chrome. It simply outputs "b", rather than any of the other values in "obj".Phonometer
K
4

If

console.log(JSON.stringify(object));

results in a

TypeError: cyclic object value

Then you may want to print like this:

var output = '';
for (property in object) {
  output += property + ': ' + object[property]+'; ';
}
console.log(output);
Kussell answered 26/10, 2013 at 11:2 Comment(2)
Maybe because it only prints one level?Illfated
VERY SIMPLE i upvoted this because it worked for me right out of the box in chrome. EXCELLENTScouring
B
4

I know this is an old question, but I'd like to suggest an NPM package I've created called smart-circular, which works differently from the other ways proposed. It's specially useful if you're using big and deep objects.

Some features are:

  • Replacing circular references or simply repeated structures inside the object by the path leading to its first occurrence (not just the string [circular]);

  • By looking for circularities in a breadth-first search, the package ensures this path is as small as possible, which is important when dealing with very big and deep objects, where the paths can get annoyingly long and difficult to follow (the custom replacement in JSON.stringify does a DFS);

  • Allows personalised replacements, handy to simplify or ignore less important parts of the object;

  • Finally, the paths are written exactly in the way necessary to access the field referenced, which can help you debugging.

Budgerigar answered 18/8, 2015 at 10:8 Comment(1)
This is nice, although it's removing duplicates as well, not just circular links. (well, if you define "circular" as meaning that doing a depth-first recursion, without safeguards, would result in a call-stack going over the same entry multiple/infinite times) Has a use, but not what some would define as "circular" (ie. causing infinite recursion).Auriol
G
3

The second argument to JSON.stringify() also allows you to specify an array of key names that should be preserved from every object it encounters within your data. This may not work for all use cases, but is a much simpler solution.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify

var obj = {
    a: "foo",
    b: this
}

var json = JSON.stringify(obj, ['a']);
console.log(json);
// {"a":"foo"}

Note: Strangely, the object definition from OP does not throw a circular reference error in the latest Chrome or Firefox. The definition in this answer was modified so that it did throw an error.


Gaffney answered 1/8, 2018 at 21:55 Comment(0)
M
2

I found circular-json library on github and it worked well for my problem.

Some good features I found useful:

  • Supports multi-platform usage but I only tested it with node.js so far.
  • API is same so all you need to do is include and use it as a JSON replacement.
  • It have it's own parsing method so you can convert the 'circular' serialized data back to object.
Munday answered 4/8, 2015 at 19:42 Comment(4)
This library threw an error for me so I have to look for another. ERROR TypeError: toISOString is not a function at String.toJSON (<anonymous>) at Object.<anonymous> (localhost:8100/build/polyfills.js:1:3458) at JSON.stringify (<anonymous>) at Object.stringifyRecursion [as stringify] (localhost:8100/build/main.js:258450:15)Cookery
@MarkEllul I've written the comment in 2015 and if I'll see a better alternative I'll post it here with an edit. I still end up the same issue in daily work occasionally and I usually prefer my own manual functions in a recursive manner with a proper/safe inspection. I would suggest checking out functional programming practices if you are unfamiliar, usually, it's easing up this kind of recursive operations as being less tricky and more reliable.Munday
Also getting "toISOString is not a function" trying to stringify an event and re-send it in a cypress testBayer
Yeah, it was working fine back in 2013. Need to updateMunday
L
2

To update the answer of overriding the way JSON works (probably not recommended, but super simple), don't use circular-json (it's deprecated). Instead, use the successor, flatted:

https://www.npmjs.com/package/flatted

Borrowed from the old answer above from @user1541685 , but replaced with the new one:

npm i --save flatted

then in your js file

const CircularJSON = require('flatted');
const json = CircularJSON.stringify(obj);
Liponis answered 28/6, 2019 at 14:11 Comment(0)
L
2

This code will fail for circular reference:

    JSON.stringify(circularReference);
// TypeError: cyclic object value

Use the below code:

 const getCircularReplacer = () => {
  const seen = new WeakSet();
  return (key, value) => {
    if (typeof value === "object" && value !== null) {
      if (seen.has(value)) {
        return;
      }
      seen.add(value);
    }
    return value;
  };
};

JSON.stringify(circularReference, getCircularReplacer());
Laine answered 1/2, 2021 at 7:1 Comment(1)
This remove all object used 2 times or more, not only referencesRockwood
B
1

I resolve this problem like this:

var util = require('util');

// Our circular object
var obj = {foo: {bar: null}, a:{a:{a:{a:{a:{a:{a:{hi: 'Yo!'}}}}}}}};
obj.foo.bar = obj;

// Generate almost valid JS object definition code (typeof string)
var str = util.inspect(b, {depth: null});

// Fix code to the valid state (in this example it is not required, but my object was huge and complex, and I needed this for my case)
str = str
    .replace(/<Buffer[ \w\.]+>/ig, '"buffer"')
    .replace(/\[Function]/ig, 'function(){}')
    .replace(/\[Circular]/ig, '"Circular"')
    .replace(/\{ \[Function: ([\w]+)]/ig, '{ $1: function $1 () {},')
    .replace(/\[Function: ([\w]+)]/ig, 'function $1(){}')
    .replace(/(\w+): ([\w :]+GMT\+[\w \(\)]+),/ig, '$1: new Date("$2"),')
    .replace(/(\S+): ,/ig, '$1: null,');

// Create function to eval stringifyed code
var foo = new Function('return ' + str + ';');

// And have fun
console.log(JSON.stringify(foo(), null, 4));
Botelho answered 26/2, 2016 at 8:10 Comment(1)
This pretty much worked for me but it seems like classes were being represented like _class: ClassName { data: "here" }, so I added the following rule .replace(/(\w+) {/g, '{ __ClassName__: "$1", '). In my case I was trying to see what an http request object looked like.Shikoku
A
1

Although this has been answered sufficiently, you could also explicitly delete the property in question before stringification using the delete operator.

delete obj.b; 
const jsonObject = JSON.stringify(obj);

delete operator

this will remove the need to build or maintain complex logic to remove circular references.

Allwein answered 16/5, 2019 at 23:12 Comment(0)
Z
1
function myStringify(obj, maxDeepLevel = 2) {
  if (obj === null) {
    return 'null';
  }
  if (obj === undefined) {
    return 'undefined';
  }
  if (maxDeepLevel < 0 || typeof obj !== 'object') {
    return obj.toString();
  }
  return Object
    .entries(obj)
    .map(x => x[0] + ': ' + myStringify(x[1], maxDeepLevel - 1))
    .join('\r\n');
}
Zamarripa answered 28/8, 2019 at 14:20 Comment(0)
M
1

Here's a solution that:

  • removes cycles only (and not all duplicate object references, as do most of the solutions posted here so far),
  • is not unnecessarily verbose,
  • is fast,
  • does not require any library dependency.
function replaceCycles(obj, replacement = undefined, seen = new WeakSet()) {
  if (typeof obj === 'object')
    if (seen.has(obj))
      return replacement 
    else {
      seen.add(obj)
      const newObj = {}
      for (const key in obj)
        newObj[key] = replaceCycles(obj[key], replacement, seen)
      seen.delete(obj)
      return newObj
    }
  else
    return obj
}

Usage:

const a = {
  b: 'v1',
  c: {
    d: 'v2'
  }
}

a.e = a.c
a.c.f = a.c

console.log(JSON.stringify(replaceCycles(a, '[CYCLE]')))

Output:

"{'b':'v1','c':{'d':'v2','f':'[CYCLE]'},'e':{'d':'v2','f':'[CYCLE]'}}"
Mogador answered 25/3, 2022 at 15:33 Comment(0)
A
0

an other solution for resolving this issue with these kind of objects is that using this library

https://github.com/ericmuyser/stringy

its simple and you can in a few simple step solve this.

Areopagite answered 20/5, 2015 at 10:52 Comment(0)
A
0

Based on the other answers I end up with the following code. It works pretty well with circular references, objects with custom constructors.

From the given object to be serialized,

  • Cache all the object you come across while traversing the object and assign each of them a unique hashID (an auto-incrementing number also works)
  • Once a circular reference is found mark that field in the new object as circular and store the hashID of the original object as an attribute.

Github Link - DecycledJSON

DJSHelper = {};
DJSHelper.Cache = [];
DJSHelper.currentHashID = 0;
DJSHelper.ReviveCache = [];

// DOES NOT SERIALIZE FUNCTION
function DJSNode(name, object, isRoot){
    this.name = name;
    // [ATTRIBUTES] contains the primitive fields of the Node
    this.attributes = {};

    // [CHILDREN] contains the Object/Typed fields of the Node
    // All [CHILDREN] must be of type [DJSNode]
    this.children = []; //Array of DJSNodes only

    // If [IS-ROOT] is true reset the Cache and currentHashId
    // before encoding
    isRoot = typeof isRoot === 'undefined'? true:isRoot;
    this.isRoot = isRoot;
    if(isRoot){
        DJSHelper.Cache = [];
        DJSHelper.currentHashID = 0;

        // CACHE THE ROOT
        object.hashID = DJSHelper.currentHashID++;
        DJSHelper.Cache.push(object);
    }

    for(var a in object){
        if(object.hasOwnProperty(a)){
            var val = object[a];

            if (typeof val === 'object') {
                // IF OBJECT OR NULL REF.

                /***************************************************************************/
                // DO NOT REMOVE THE [FALSE] AS THAT WOULD RESET THE [DJSHELPER.CACHE]
                // AND THE RESULT WOULD BE STACK OVERFLOW
                /***************************************************************************/
                if(val !== null) {
                    if (DJSHelper.Cache.indexOf(val) === -1) {
                        // VAL NOT IN CACHE
                        // ADD THE VAL TO CACHE FIRST -> BEFORE DOING RECURSION
                        val.hashID = DJSHelper.currentHashID++;
                        //console.log("Assigned", val.hashID, "to", a);
                        DJSHelper.Cache.push(val);

                        if (!(val instanceof Array)) {
                            // VAL NOT AN [ARRAY]
                            try {
                                this.children.push(new DJSNode(a, val, false));
                            } catch (err) {
                                console.log(err.message, a);
                                throw err;
                            }
                        } else {
                            // VAL IS AN [ARRAY]
                            var node = new DJSNode(a, {
                                array: true,
                                hashID: val.hashID // HashID of array
                            }, false);
                            val.forEach(function (elem, index) {
                                node.children.push(new DJSNode("elem", {val: elem}, false));
                            });
                            this.children.push(node);
                        }
                    } else {
                        // VAL IN CACHE
                        // ADD A CYCLIC NODE WITH HASH-ID
                        this.children.push(new DJSNode(a, {
                            cyclic: true,
                            hashID: val.hashID
                        }, false));
                    }
                }else{
                    // PUT NULL AS AN ATTRIBUTE
                    this.attributes[a] = 'null';
                }
            } else if (typeof val !== 'function') {
                // MUST BE A PRIMITIVE
                // ADD IT AS AN ATTRIBUTE
                this.attributes[a] = val;
            }
        }
    }

    if(isRoot){
        DJSHelper.Cache = null;
    }
    this.constructorName = object.constructor.name;
}
DJSNode.Revive = function (xmlNode, isRoot) {
    // Default value of [isRoot] is True
    isRoot = typeof isRoot === 'undefined'?true: isRoot;
    var root;
    if(isRoot){
        DJSHelper.ReviveCache = []; //Garbage Collect
    }
    if(window[xmlNode.constructorName].toString().indexOf('[native code]') > -1 ) {
        // yep, native in the browser
        if(xmlNode.constructorName == 'Object'){
            root = {};
        }else{
            return null;
        }
    }else {
        eval('root = new ' + xmlNode.constructorName + "()");
    }

    //CACHE ROOT INTO REVIVE-CACHE
    DJSHelper.ReviveCache[xmlNode.attributes.hashID] = root;

    for(var k in xmlNode.attributes){
        // PRIMITIVE OR NULL REF FIELDS
        if(xmlNode.attributes.hasOwnProperty(k)) {
            var a = xmlNode.attributes[k];
            if(a == 'null'){
                root[k] = null;
            }else {
                root[k] = a;
            }
        }
    }

    xmlNode.children.forEach(function (value) {
        // Each children is an [DJSNode]
        // [Array]s are stored as [DJSNode] with an positive Array attribute
        // So is value

        if(value.attributes.array){
            // ITS AN [ARRAY]
            root[value.name] = [];
            value.children.forEach(function (elem) {
                root[value.name].push(elem.attributes.val);
            });
            //console.log("Caching", value.attributes.hashID);
            DJSHelper.ReviveCache[value.attributes.hashID] = root[value.name];
        }else if(!value.attributes.cyclic){
            // ITS AN [OBJECT]
            root[value.name] = DJSNode.Revive(value, false);
            //console.log("Caching", value.attributes.hashID);
            DJSHelper.ReviveCache[value.attributes.hashID] = root[value.name];
        }
    });

    // [SEPARATE ITERATION] TO MAKE SURE ALL POSSIBLE
    // [CYCLIC] REFERENCES ARE CACHED PROPERLY
    xmlNode.children.forEach(function (value) {
        // Each children is an [DJSNode]
        // [Array]s are stored as [DJSNode] with an positive Array attribute
        // So is value

        if(value.attributes.cyclic){
            // ITS AND [CYCLIC] REFERENCE
            root[value.name] = DJSHelper.ReviveCache[value.attributes.hashID];
        }
    });

    if(isRoot){
        DJSHelper.ReviveCache = null; //Garbage Collect
    }
    return root;
};

DecycledJSON = {};
DecycledJSON.stringify = function (obj) {
    return JSON.stringify(new DJSNode("root", obj));
};
DecycledJSON.parse = function (json, replacerObject) {
    // use the replacerObject to get the null values
    return DJSNode.Revive(JSON.parse(json));
};
DJS = DecycledJSON;

Example Usage 1:

var obj = {
    id:201,
    box: {
        owner: null,
        key: 'storm'
    },
    lines:[
        'item1',
        23
    ]
};

console.log(obj); // ORIGINAL

// SERIALIZE AND THEN PARSE
var jsonObj = DJS.stringify(obj);
console.log(DJS.parse(jsonObj));

Example Usage 2:

// PERSON OBJECT

function Person() {
    this.name = null;
    this.child = null;
    this.dad = null;
    this.mom = null;
}
var Dad = new Person();
Dad.name = 'John';
var Mom = new Person();
Mom.name = 'Sarah';
var Child = new Person();
Child.name = 'Kiddo';

Dad.child = Mom.child = Child;
Child.dad = Dad;
Child.mom = Mom;

console.log(Child); // ORIGINAL

// SERIALIZE AND THEN PARSE
var jsonChild = DJS.stringify(Child);
console.log(DJS.parse(jsonChild));
Africander answered 14/3, 2016 at 16:0 Comment(0)
C
0

I know this question is old and has lots of great answers but I post this answer because of it's new flavor (es5+)

Object.defineProperties(JSON, {
  refStringify: {
    value: function(obj) {

      let objMap = new Map();
      let stringified = JSON.stringify(obj,
        function(key, value) {

          // only for objects
          if (typeof value == 'object') {
            // If has the value then return a reference to it
            if (objMap.has(value))
              return objMap.get(value);

            objMap.set(value, `ref${objMap.size + 1}`);
          }
          return value;
        });
      return stringified;
    }
  },
  refParse: {
    value: function(str) {

      let parsed = JSON.parse(str);
      let objMap = _createObjectMap(parsed);
      objMap.forEach((value, key) => _replaceKeyWithObject(value, key));
      return parsed;
    }
  },
});

// *************************** Example
let a = {
  b: 32,
  c: {
    get a() {
        return a;
      },
      get c() {
        return a.c;
      }
  }
};
let stringified = JSON.refStringify(a);
let parsed = JSON.refParse(stringified, 2);
console.log(parsed, JSON.refStringify(parsed));
// *************************** /Example

// *************************** Helper
function _createObjectMap(obj) {

  let objMap = new Map();
  JSON.stringify(obj, (key, value) => {
    if (typeof value == 'object') {
      if (objMap.has(value))
        return objMap.get(value);
      objMap.set(value, `ref${objMap.size + 1}`);

    }
    return value;
  });
  return objMap;
}

function _replaceKeyWithObject(key, obj, replaceWithObject = obj) {

  Object.keys(obj).forEach(k => {

    let val = obj[k];
    if (val == key)
      return (obj[k] = replaceWithObject);
    if (typeof val == 'object' && val != replaceWithObject)
      _replaceKeyWithObject(key, val, replaceWithObject);
  });
}
Carrycarryall answered 25/1, 2017 at 10:31 Comment(0)
F
0

Try this:

var obj = {
    a: "foo",
    b: obj
};

var circular_replacer = (value) => {
    var seen = [];
    if (value != null && typeof value == "object") {
        if (seen.indexOf(value) >= 0) return;
        seen.push(value);
    }
    return value;
};

obj = circular_replacer(obj);
Forwardlooking answered 2/2, 2018 at 10:35 Comment(2)
Shouldn't there be, like, few more lines of code after the seen.push(value) =-D? Like for (var key in value) {value[key] = circular_replacer(value[key]);}Hast
Code-only answers are discouraged. Please click on edit and add some words summarising how your code addresses the question, or perhaps explain how your answer differs from the previous answer/answers. From ReviewLuteal
Z
0

You could try the JSON parser library: treedoc. it supports circular references and also dedupes the repeated objects with references.

yarn add treedoc

import {TD} from 'treedoc'
TD.stringify(obj);

If you want more customization

import {TD, TDEncodeOption} from 'treedoc'

const opt = new TDEncodeOption();
opt.coderOption.setShowType(true).setShowFunction(true);
opt.jsonOption.setIndentFactor(2);
return TD.stringify(obj, opt);

The generated JSON file can be viewed by the viewer http://treedoc.org, which supports the navigation through JSON node references.

[shameless plug] I'm the author of this library

Zaratite answered 12/7, 2020 at 7:6 Comment(0)
A
0

Most of the answers in this thread are catered to use with JSON.stringify specifically -- they do not show how to actually remove circular-references in the original object-tree. (well, short of calling JSON.parse again afterward -- which requires reassignment, and has a higher performance impact)

For removing circular-references from the source object-tree, you can use a function such as this: https://mcmap.net/q/36583/-detecting-and-fixing-circular-references-in-javascript

These general-purpose circular-reference-remover functions can then be used to make subsequent calls to circular-reference-sensitive functions (like JSON.stringify) safe:

const objTree = {normalProp: true};
objTree.selfReference = objTree;
RemoveCircularLinks(objTree); // without this line, the JSON.stringify call errors
console.log(JSON.stringify(objTree));
Auriol answered 18/9, 2020 at 9:2 Comment(0)
C
0

We use object-scan for our data processing and it might be a viable solution here. This is how it could work (also pruning arrays correctly)

.as-console-wrapper {max-height: 100% !important; top: 0}
<script type="module">
import objectScan from 'https://cdn.jsdelivr.net/npm/[email protected]/lib/index.min.js';

const prune = (data) => objectScan(['**'], {
  rtn: 'count',
  filterFn: ({ isCircular, parent, property }) => {
    if (isCircular) {
      if (Array.isArray(parent)) {
        parent.splice(property, 1);
      } else {
        delete parent[property];
      }
      return true;
    }
    return false;
  },
  breakFn: ({ isCircular }) => isCircular === true
})(data);

const obj = { a: 'foo', c: [0] };
obj.b = obj;
obj.c.push(obj);
console.log(obj);
// => <ref *1> { a: 'foo', c: [ 0, [Circular *1] ], b: [Circular *1] }

console.log(prune(obj)); // returns circular counts
// => 2

console.log(obj);
// => { a: 'foo', c: [ 0 ] }
</script>

Disclaimer: I'm the author of object-scan

Cosmos answered 25/10, 2020 at 19:39 Comment(0)
H
0

I created following method for my LoggingUtilities class. Following method takes source and target objects, and assign source to target by given maxLevel.

  static assignObjectByLevel(
    sourceObject: any,
    targetObject: any,
    currentLevel: number = 0,
    maxLevel: number = 3,
    showUndefinedValues = false
  ): any {
    if (currentLevel >= maxLevel) {
      return;
    }

    const objQueue = [];
    for (const key in sourceObject) {
      if (sourceObject.hasOwnProperty(key)) {
        const value = sourceObject[key];
        if (typeof value === "object") {
          objQueue.push({ key, value });
        } else {
          targetObject[key] = value;
        }
      } else {
        if (showUndefinedValues) {
          targetObject[key] = "undefined/null";
        }
      }
    }

    while (objQueue.length > 0) {
      const objVal = objQueue.pop();
      currentLevel++;
      targetObject[objVal.key] = {};
      this.assignObjectByLevel(
        objVal.value,
        targetObject[objVal.key],
        currentLevel,
        maxLevel,
        false
      );
    }
  }

Usage Example:

   const logObjParam = {
      level1: "value1",
      level2: {
        value2: "value2",
        level3: {
          value3: "value3",
          level4: {
            value4: " value4",
            level5: {
              value5: " value5",
            },
          },
        },
      },
    };

 let logObj = {};
 this.assignObjectByLevel(logObjParam, logObj);

Result:

{
  "level1": "value1",
  "level2": {
    "value2": "value2",
    "level3": {
      "value3": "value3",
      "level4": {}
    }
  }
}
Hazelton answered 11/4, 2021 at 21:6 Comment(0)
I
0

superserial fully serializes JavaScript objects.

https://github.com/denostack/superserial

Usage:

const serializer = new Serializer();

const nodes = [{ self: null as any, siblings: [] as any[] }, {
  self: null as any,
  siblings: [] as any[],
}];
nodes[0].self = nodes[0];
nodes[0].siblings = nodes;
nodes[1].self = nodes[1];
nodes[1].siblings = nodes;

const serialized = serializer.serialize(nodes);

console.log(serialized);

output:

[$1,$2];{"self":$1,"siblings":$0};{"self":$2,"siblings":$0}
Impunity answered 29/3, 2022 at 13:45 Comment(0)
B
0

I had the same problem, I carefully red all the other excellent answers to this question, but none of them satisfies me for two reasons:

  1. some of them disrupts the original object;
  2. if I cut/paste the payload in a source code, it is no equivalent to the original object.

To solve all the three problem at once (the one in the question plus the other two I had) I wrote next-json.

  • It offers an interface analogue to the JSON one.
  • The payload can be cut/pasted in a source code to reproduce the original object.
  • It exposes polyfill for express and fetch.

Basic usage.

import { NJSON } from "next-json";

const obj = { test: Infinity };
const set = new Set();
const arr = [NaN, obj, set];

set.add(obj);
set.add(arr);
arr.push(arr);

console.log(NJSON.stringify(arr));
// ((A,B)=>{B.push(A,new Set([A,B]),B);return B})({"test":Infinity},[NaN])

With polyfill.

Server side:

import express from "express";
import { expressNJSON } from "next-json";

const app = express();

app.use(expressNJSON()); // install the polyfill
app.all("/mirror", (req, res) => res.njson(req.body)); // there is an 'n' more than usual
app.listen(3000);

Client side:

import { NJSON, fetchNJSON } from "next-json";

fetchNJSON(); // install the polyfill

const payload = { infinity: Infinity };
payload.circular = payload;

const response = await fetch("http://localhost:3000/mirror", {
  body: NJSON.stringify(payload), // there is an 'N' more than usual
  headers: { "Content-Type": "application/njson" }, // there is an 'n' more than usual
  method: "POST"
});
const body = await response.njson(); // there is an 'n' more than usual

Here payload deep equals payload.circular, which deep equals body, which deep equals body.circular, which deep equals req.body in server side, which deep equals req.body.circular in server side.

Bathilda answered 24/2 at 0:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.