diff --git a/packages/utils/src/normalize.ts b/packages/utils/src/normalize.ts index c88ab00b510f..f40304374137 100644 --- a/packages/utils/src/normalize.ts +++ b/packages/utils/src/normalize.ts @@ -77,16 +77,6 @@ function visit( ): Primitive | ObjOrArray { const [memoize, unmemoize] = memo; - // If the value has a `toJSON` method, see if we can bail and let it do the work - const valueWithToJSON = value as unknown & { toJSON?: () => Primitive | ObjOrArray }; - if (valueWithToJSON && typeof valueWithToJSON.toJSON === 'function') { - try { - return valueWithToJSON.toJSON(); - } catch (err) { - // pass (The built-in `toJSON` failed, but we can still try to do it ourselves) - } - } - // Get the simple cases out of the way first if (value === null || (['number', 'boolean', 'string'].includes(typeof value) && !isNaN(value))) { return value as Primitive; @@ -120,6 +110,18 @@ function visit( return '[Circular ~]'; } + // If the value has a `toJSON` method, we call it to extract more information + const valueWithToJSON = value as unknown & { toJSON?: () => unknown }; + if (valueWithToJSON && typeof valueWithToJSON.toJSON === 'function') { + try { + const jsonValue = valueWithToJSON.toJSON(); + // We need to normalize the return value of `.toJSON()` in case it has circular references + return visit('', jsonValue, depth - 1, maxProperties, memo); + } catch (err) { + // pass (The built-in `toJSON` failed, but we can still try to do it ourselves) + } + } + // 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 // because we haven't yet reached the max depth. Create an accumulator to hold the results of visiting each // property/entry, and keep track of the number of items we add to it. diff --git a/packages/utils/test/normalize.test.ts b/packages/utils/test/normalize.test.ts index e5d01de4e962..f9d909798d37 100644 --- a/packages/utils/test/normalize.test.ts +++ b/packages/utils/test/normalize.test.ts @@ -285,6 +285,30 @@ describe('normalize()', () => { // @ts-ignore target lacks a construct signature expect(normalize([{ a }, { b: new B() }, c])).toEqual([{ a: 1 }, { b: 2 }, 3]); }); + + test('should return a normalized object even if toJSON throws', () => { + const subject = { a: 1, foo: 'bar' } as any; + subject.toJSON = () => { + throw new Error("I'm faulty!"); + }; + expect(normalize(subject)).toEqual({ a: 1, foo: 'bar', toJSON: '[Function: ]' }); + }); + + test('should return an object without circular references when toJSON returns an object with circular references', () => { + const subject: any = {}; + subject.toJSON = () => { + const egg: any = {}; + egg.chicken = egg; + return egg; + }; + expect(normalize(subject)).toEqual({ chicken: '[Circular ~]' }); + }); + + test('should detect circular reference when toJSON returns the original object', () => { + const subject: any = {}; + subject.toJSON = () => subject; + expect(normalize(subject)).toEqual('[Circular ~]'); + }); }); describe('changes unserializeable/global values/classes to its string representation', () => {