Skip to content

fix(utils): Handle toJSON methods that return circular references #5323

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

Merged
merged 3 commits into from
Jun 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 12 additions & 10 deletions packages/utils/src/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,6 @@ function visit(
): Primitive | ObjOrArray<unknown> {
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<unknown> };
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;
Expand Down Expand Up @@ -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.
Expand Down
24 changes: 24 additions & 0 deletions packages/utils/test/normalize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: <anonymous>]' });
});

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', () => {
Expand Down