Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optionally, replace circular references with the key it points to #1

Open
binyamin opened this issue Apr 1, 2022 · 6 comments
Open
Labels
enhancement New feature or request help wanted Extra attention is needed

Comments

@binyamin
Copy link

binyamin commented Apr 1, 2022

Currently, circular references are each replaced with the string [Circular]. It would be helpful to identify which key the circular reference points to.

const foo = {a: true};
foo.b = foo.a;

console.log(safeStringify(foo));
//=> '{ "a": true, "b": "[Circular]" }'

console.log(safeStringify(foo, { trace: true } ));
//=> '{ "a": true, "b": "[Circular *a]" }'
@sindresorhus
Copy link
Owner

Not something I need, but a good pull request would be welcomed.

@sindresorhus sindresorhus added enhancement New feature or request help wanted Extra attention is needed labels Apr 10, 2022
@GeorgeGkas
Copy link

@binyamin your example does not produce a circulare reference, foo.a will be resolved to true. I can find the value in this issue, but there are many edge cases to consider. Suppose the following example:

const foo = {a: true};
foo.b = foo;
foo.c = foo.b;

console.log(safeStringify(foo, {trace: true}));

There are multiple correct outputs we can expect:

Output 1:

{"a":true,"b":"[Circular *]","c":"[Circular *]"}

Output 2:

{"a":true,"b":"[Circular *]","c":"[Circular *b]"}

In the first output, we resolve from c -> b -> *, where * is the root object. The second output is also valid but stops the trace at the first reference which is b.

Another case which has beeen retrieved from the tests is the following:

const fixture = {
    a: true,
};

fixture.b = fixture;

console.log(safeStringify({x: fixture, y: fixture}, optionsWithTrace));

A possible output would be:

{"x":{"a":true,"b":"[Circular *x]"},"y":"[Circular *x]"}

Here both keys x and y reference the root object, but because the value of x is the root object itself, as it is for y, the final output uses the x key to resolve the reference, which is correct algorithmically.

I'm going to push a PR to track a possible solution.

Due to the nature of circular references and the makeCircularReplacer function we cannot guarrantee the final indicator value. The algorithm will always return the correct result, but we cannot know easily ahead of time which key it will choose. What we know is that every time a new circular reference appears, we push the corresponding key and value to a WeakMap. Depending on the object traversal the final value is calculated.

@sindresorhus
Copy link
Owner

If anyone wants to work on this, see the initial attempt and feedback here: #3

@condorheroblog
Copy link

I learned about the content of this PR - #3. According to the latest code, the latest implementation of the trace function is shown below:

function safeStringifyReplacer(seen, trace) {
	return function (key, value) {
		if (value !== null && typeof value === 'object') {
			if (seen.has(value)) {
				return trace ? `[Circular *${seen.get(value)}]` : '[Circular]';
			}

			seen.set(value, key);

			const newValue = Array.isArray(value) ? [] : {};

			for (const [key2, value2] of Object.entries(value)) {
				newValue[key2] = safeStringifyReplacer(seen, trace)(key2, value2);
			}

			seen.delete(value);

			return newValue;
		}

		return value;
	};
}

export default function safeStringify(object, {indentation, trace} = {}) {
	const seen = new WeakMap();
	return JSON.stringify(object, safeStringifyReplacer(seen, trace), indentation);
}

Assuming there is an object:

const obj = {
	a: 'a',
	b: {
		prop1: 'prop1',
		b: {}
	}
};

Set circular reference:

obj.b.b.f = obj.b

Run safeStringify(obj, { trace: true })

'{"a":"a","b":{"prop1":"prop1","b":{"f":"[Circular *b]"}}}'

If the circular reference set above is another b

obj.b.b.f = obj.b.b

There is no difference in the results of running the safeStringify function. Which one of the circular references cannot be intuitively determined.

So I think when opening trace, the key should not be directly displayed, but the path of the key should be displayed.

The path of the key, just like when the second parameter of Lodash's get function is a string.

var object = { 'a': [{ 'b': { 'c': 3 } }] };
 
_.get(object, 'a[0].b.c');

The expected outcome should be the following:

  • obj.b.b.f = obj.b
'{"a":"a","b":{"prop1":"prop1","b":{"f":"[Circular *b]"}}}'
  • obj.b.b.f = obj.b.b
'{"a":"a","b":{"prop1":"prop1","b":{"f":"[Circular *b.b]"}}}'

What is your opinion?

@sindresorhus
Copy link
Owner

I expect it to show the full path (the second one).

You could use this as inspiration: https://github.com/sindresorhus/decircular/blob/main/index.js

@condorheroblog
Copy link

condorheroblog commented Nov 15, 2023

I like this library — decircular. Although I will not invent from scratch, I have learned a lot from your code, and I am grateful from the bottom of my heart.

I have one last question that I would like to ask you., regarding the case where the access path is a number:

// property a is an array
const obj = {
	"a": [
		{
			"b": {
				"c": 3
			}
		}
	]
}


// or property a is an object, but key is a number
const obj = {
	"a": {
		0: {
			"b": {
				"c": 3
			}
		}
	}
}

If you want to read the value of attribute c, the valid access path in the above two cases in JS should be: obj.a[0].b.c, not obj.a.0.b.c


In decircular

const object1 = {
	a: 1,
	b: [
		{
			c: 2,
		}
	]
};

const object2 = {
	a: 1,
	b: {
		0: {
			c: 2,
		}
	}
};

object1.b[0].d = object1.b[0]; // Creates a circular reference
object2.b[0].d = object2.b[0]; // Creates a circular reference


console.log(decircular(object1));
console.log(decircular(object2));

The reference path output in the log is [Circular *b.0] instead of the available [Circular *b[0]]. Do you think it should be changed to the path available in JS ([Circular *b[0 ]]), but it seems that [Circular *b.0] is more intuitive, because only one square bracket will appear.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants