Skip to content

Commit 893e157

Browse files
lforstLms24
andauthored
fix(utils): Handle toJSON methods that return circular references (#5323)
Co-authored-by: Lukas Stracke <[email protected]>
1 parent f15fb00 commit 893e157

File tree

2 files changed

+36
-10
lines changed

2 files changed

+36
-10
lines changed

packages/utils/src/normalize.ts

+12-10
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,6 @@ function visit(
7777
): Primitive | ObjOrArray<unknown> {
7878
const [memoize, unmemoize] = memo;
7979

80-
// If the value has a `toJSON` method, see if we can bail and let it do the work
81-
const valueWithToJSON = value as unknown & { toJSON?: () => Primitive | ObjOrArray<unknown> };
82-
if (valueWithToJSON && typeof valueWithToJSON.toJSON === 'function') {
83-
try {
84-
return valueWithToJSON.toJSON();
85-
} catch (err) {
86-
// pass (The built-in `toJSON` failed, but we can still try to do it ourselves)
87-
}
88-
}
89-
9080
// Get the simple cases out of the way first
9181
if (value === null || (['number', 'boolean', 'string'].includes(typeof value) && !isNaN(value))) {
9282
return value as Primitive;
@@ -120,6 +110,18 @@ function visit(
120110
return '[Circular ~]';
121111
}
122112

113+
// If the value has a `toJSON` method, we call it to extract more information
114+
const valueWithToJSON = value as unknown & { toJSON?: () => unknown };
115+
if (valueWithToJSON && typeof valueWithToJSON.toJSON === 'function') {
116+
try {
117+
const jsonValue = valueWithToJSON.toJSON();
118+
// We need to normalize the return value of `.toJSON()` in case it has circular references
119+
return visit('', jsonValue, depth - 1, maxProperties, memo);
120+
} catch (err) {
121+
// pass (The built-in `toJSON` failed, but we can still try to do it ourselves)
122+
}
123+
}
124+
123125
// At this point we know we either have an object or an array, we haven't seen it before, and we're going to recurse
124126
// because we haven't yet reached the max depth. Create an accumulator to hold the results of visiting each
125127
// property/entry, and keep track of the number of items we add to it.

packages/utils/test/normalize.test.ts

+24
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,30 @@ describe('normalize()', () => {
285285
// @ts-ignore target lacks a construct signature
286286
expect(normalize([{ a }, { b: new B() }, c])).toEqual([{ a: 1 }, { b: 2 }, 3]);
287287
});
288+
289+
test('should return a normalized object even if toJSON throws', () => {
290+
const subject = { a: 1, foo: 'bar' } as any;
291+
subject.toJSON = () => {
292+
throw new Error("I'm faulty!");
293+
};
294+
expect(normalize(subject)).toEqual({ a: 1, foo: 'bar', toJSON: '[Function: <anonymous>]' });
295+
});
296+
297+
test('should return an object without circular references when toJSON returns an object with circular references', () => {
298+
const subject: any = {};
299+
subject.toJSON = () => {
300+
const egg: any = {};
301+
egg.chicken = egg;
302+
return egg;
303+
};
304+
expect(normalize(subject)).toEqual({ chicken: '[Circular ~]' });
305+
});
306+
307+
test('should detect circular reference when toJSON returns the original object', () => {
308+
const subject: any = {};
309+
subject.toJSON = () => subject;
310+
expect(normalize(subject)).toEqual('[Circular ~]');
311+
});
288312
});
289313

290314
describe('changes unserializeable/global values/classes to its string representation', () => {

0 commit comments

Comments
 (0)