Skip to content

Commit a7b4e88

Browse files
authored
prevent circular deserialization (#107)
fix "Maximum call stack size exceeded" error. closes #106 #107
1 parent 9de7a78 commit a7b4e88

File tree

2 files changed

+205
-57
lines changed

2 files changed

+205
-57
lines changed

Diff for: lib/JSONAPISerializer.js

+45-57
Original file line numberDiff line numberDiff line change
@@ -438,9 +438,10 @@ module.exports = class JSONAPISerializer {
438438
* @param {object} data JSON API resource data.
439439
* @param {string} [schema='default'] resource's schema name.
440440
* @param {Map<string, object>} included Included resources.
441+
* @param {string[]} lineage resource identifiers already deserialized to prevent circular references.
441442
* @returns {object} deserialized data.
442443
*/
443-
deserializeResource(type, data, schema = 'default', included) {
444+
deserializeResource(type, data, schema = 'default', included, lineage = []) {
444445
if (typeof type === 'object') {
445446
type = typeof type.type === 'function' ? type.type(data) : get(data, type.type);
446447
}
@@ -487,73 +488,54 @@ module.exports = class JSONAPISerializer {
487488
};
488489

489490
if (relationship.data !== undefined) {
490-
if (Array.isArray(relationship.data)) {
491-
if (relationshipOptions && relationshipOptions.alternativeKey) {
492-
set(
493-
deserializedData,
494-
relationshipOptions.alternativeKey,
495-
relationship.data.map((d) => deserializeFunction(d))
496-
);
497-
498-
if (included) {
499-
set(
500-
deserializedData,
501-
relationshipKey,
502-
relationship.data.map((d) =>
503-
this.deserializeIncluded(d.type, d.id, relationshipOptions, included)
504-
)
505-
);
506-
}
507-
} else {
508-
set(
509-
deserializedData,
510-
relationshipKey,
511-
relationship.data.map((d) =>
512-
included
513-
? this.deserializeIncluded(d.type, d.id, relationshipOptions, included)
514-
: deserializeFunction(d)
515-
)
516-
);
517-
}
518-
} else if (relationship.data === null) {
491+
if (relationship.data === null) {
519492
// null data
520493
set(
521494
deserializedData,
522495
(relationshipOptions && relationshipOptions.alternativeKey) || relationshipKey,
523496
null
524497
);
525-
} else if (relationshipOptions && relationshipOptions.alternativeKey) {
526-
set(
527-
deserializedData,
528-
relationshipOptions.alternativeKey,
529-
deserializeFunction(relationship.data)
530-
);
498+
} else {
499+
if ((relationshipOptions && relationshipOptions.alternativeKey) || !included) {
500+
set(
501+
deserializedData,
502+
(relationshipOptions && relationshipOptions.alternativeKey) || relationshipKey,
503+
Array.isArray(relationship.data)
504+
? relationship.data.map((d) => deserializeFunction(d))
505+
: deserializeFunction(relationship.data)
506+
);
507+
}
531508

532509
if (included) {
510+
const deserializeIncludedRelationship = (relationshipData) => {
511+
const lineageCopy = [...lineage];
512+
// Prevent circular relationships
513+
const isCircular = lineageCopy.includes(
514+
`${relationshipData.type}-${relationshipData.id}`
515+
);
516+
517+
if (isCircular) {
518+
return deserializeFunction(data);
519+
}
520+
521+
lineageCopy.push(`${type}-${data.id}`);
522+
return this.deserializeIncluded(
523+
relationshipData.type,
524+
relationshipData.id,
525+
relationshipOptions,
526+
included,
527+
lineageCopy
528+
);
529+
};
530+
533531
set(
534532
deserializedData,
535533
relationshipKey,
536-
this.deserializeIncluded(
537-
relationship.data.type,
538-
relationship.data.id,
539-
relationshipOptions,
540-
included
541-
)
534+
Array.isArray(relationship.data)
535+
? relationship.data.map((d) => deserializeIncludedRelationship(d))
536+
: deserializeIncludedRelationship(relationship.data)
542537
);
543538
}
544-
} else {
545-
set(
546-
deserializedData,
547-
relationshipKey,
548-
included
549-
? this.deserializeIncluded(
550-
relationship.data.type,
551-
relationship.data.id,
552-
relationshipOptions,
553-
included
554-
)
555-
: deserializeFunction(relationship.data)
556-
);
557539
}
558540
}
559541
});
@@ -578,7 +560,7 @@ module.exports = class JSONAPISerializer {
578560
return deserializedData;
579561
}
580562

581-
deserializeIncluded(type, id, relationshipOpts, included) {
563+
deserializeIncluded(type, id, relationshipOpts, included, lineage) {
582564
const includedResource = included.find(
583565
(resource) => resource.type === type && resource.id === id
584566
);
@@ -587,7 +569,13 @@ module.exports = class JSONAPISerializer {
587569
return id;
588570
}
589571

590-
return this.deserializeResource(type, includedResource, relationshipOpts.schema, included);
572+
return this.deserializeResource(
573+
type,
574+
includedResource,
575+
relationshipOpts.schema,
576+
included,
577+
lineage
578+
);
591579
}
592580

593581
/**

Diff for: test/unit/JSONAPISerializer.test.js

+160
Original file line numberDiff line numberDiff line change
@@ -1564,6 +1564,166 @@ describe('JSONAPISerializer', function() {
15641564
done();
15651565
});
15661566

1567+
it('should deserialize data with circular included', function(done) {
1568+
const Serializer = new JSONAPISerializer();
1569+
1570+
Serializer.register('article', {
1571+
relationships: {
1572+
author: { type: 'user' },
1573+
user: { type: 'user' },
1574+
profile: { type: 'profile' }
1575+
},
1576+
});
1577+
1578+
Serializer.register('user', {
1579+
relationships: {
1580+
profile: { type: 'profile' },
1581+
},
1582+
});
1583+
1584+
Serializer.register('profile', {
1585+
relationships: {
1586+
user: { type: 'user' },
1587+
},
1588+
});
1589+
1590+
const data = {
1591+
"data": {
1592+
"type": "article",
1593+
"id": "1",
1594+
"attributes": {
1595+
"title": "title"
1596+
},
1597+
"relationships": {
1598+
"author": { "data": { "type": "user", "id": "1" } },
1599+
"user": { "data": { "type": "user", "id": "1" } },
1600+
"profile": { "data": { "type": "profile", "id": "1" } }
1601+
}
1602+
},
1603+
"included": [
1604+
{
1605+
"type": "user",
1606+
"id": "1",
1607+
"attributes": {
1608+
"email": "[email protected]"
1609+
},
1610+
"relationships": {
1611+
"profile": { "data": { "type": "profile", "id": "1" } }
1612+
}
1613+
},
1614+
{
1615+
"type": "profile",
1616+
"id": "1",
1617+
"attributes": {
1618+
"firstName": "first-name",
1619+
"lastName": "last-name"
1620+
},
1621+
"relationships": {
1622+
"user": { "data": { "type": "user", "id": "1" } }
1623+
}
1624+
}
1625+
]
1626+
}
1627+
1628+
const deserializedData = Serializer.deserialize('article', data);
1629+
expect(deserializedData).to.have.property('id');
1630+
expect(deserializedData.author).to.have.property('id');
1631+
expect(deserializedData.author.profile).to.have.property('id');
1632+
expect(deserializedData.author.profile.user).to.equal('1');
1633+
expect(deserializedData.user).to.have.property('id');
1634+
expect(deserializedData.profile).to.have.property('id');
1635+
done();
1636+
});
1637+
1638+
it('should deserialize data with circular included array', function(done) {
1639+
const Serializer = new JSONAPISerializer();
1640+
1641+
Serializer.register('article', {
1642+
relationships: {
1643+
author: { type: 'user' },
1644+
profile: {type: 'profile'}
1645+
},
1646+
});
1647+
1648+
Serializer.register('user', {
1649+
relationships: {
1650+
profile: { type: 'profile' },
1651+
},
1652+
});
1653+
1654+
Serializer.register('profile', {
1655+
relationships: {
1656+
user: { type: 'user' },
1657+
profile: {type: 'profile'}
1658+
},
1659+
});
1660+
1661+
const data = {
1662+
"data": {
1663+
"type": "article",
1664+
"id": "1",
1665+
"attributes": {
1666+
"title": "title"
1667+
},
1668+
"relationships": {
1669+
"author": { "data": [{ "type": "user", "id": "1" }, { "type": "user", "id": "2" }] },
1670+
}
1671+
},
1672+
"included": [
1673+
{
1674+
"type": "user",
1675+
"id": "1",
1676+
"attributes": {
1677+
"email": "[email protected]"
1678+
},
1679+
"relationships": {
1680+
"profile": { "data": [{ "type": "profile", "id": "1" }] }
1681+
}
1682+
},
1683+
{
1684+
"type": "user",
1685+
"id": "2",
1686+
"attributes": {
1687+
"email": "[email protected]"
1688+
},
1689+
"relationships": {
1690+
"profile": { "data": [{ "type": "profile", "id": "2" }] }
1691+
}
1692+
},
1693+
{
1694+
"type": "profile",
1695+
"id": "1",
1696+
"attributes": {
1697+
"firstName": "first-name",
1698+
"lastName": "last-name"
1699+
},
1700+
"relationships": {
1701+
"user": { "data": { "type": "user", "id": "1" } }
1702+
}
1703+
},
1704+
{
1705+
"type": "profile",
1706+
"id": "2",
1707+
"attributes": {
1708+
"firstName": "first-name",
1709+
"lastName": "last-name"
1710+
},
1711+
"relationships": {
1712+
"user": { "data": { "type": "user", "id": "2" } }
1713+
}
1714+
}
1715+
]
1716+
}
1717+
1718+
const deserializedData = Serializer.deserialize('article', data);
1719+
expect(deserializedData).to.have.property('id');
1720+
expect(deserializedData).to.have.property('author').to.be.instanceof(Array).to.have.length(2);
1721+
expect(deserializedData.author[0]).to.have.property('profile').to.be.instanceof(Array).to.have.length(1);
1722+
expect(deserializedData.author[0].profile[0]).to.have.property('user').to.equal('1');
1723+
expect(deserializedData.author[1].profile[0]).to.have.property('user').to.equal('2');
1724+
done();
1725+
});
1726+
15671727
it('should deserialize with missing included relationship', function(done) {
15681728
const Serializer = new JSONAPISerializer();
15691729
Serializer.register('articles', {

0 commit comments

Comments
 (0)