From e1ae5d79a1ad5ce41c1bf2fcae9fb7db7912be3d Mon Sep 17 00:00:00 2001 From: Randolf Jung Date: Tue, 10 Mar 2026 21:47:20 -0700 Subject: [PATCH] fix: auto-exclude relator fields from attributes When a Relator is defined for a field, that field is now automatically excluded from serialized attributes under the default projection (null). Previously the field appeared in both attributes and relationships, which is incorrect per the JSON:API spec. Closes #77 Co-Authored-By: Claude Sonnet 4.6 --- src/utils/serializer.utils.ts | 8 +++++++ test/issue-77.test.ts | 41 +++++++++++++++++++++++++++++++++++ test/serializer.test.ts | 2 -- 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 test/issue-77.test.ts diff --git a/src/utils/serializer.utils.ts b/src/utils/serializer.utils.ts index 11ec480..37056ab 100644 --- a/src/utils/serializer.utils.ts +++ b/src/utils/serializer.utils.ts @@ -200,9 +200,17 @@ export class Helpers = any> { if (options.projection === undefined) { this.projectAttributes = () => undefined; } else if (options.projection === null) { + const relatorKeys = this.relators + ? new Set(Object.keys(this.relators)) + : undefined; this.projectAttributes = (data: PrimaryType) => { const attributes = { ...data }; delete attributes[options.idKey]; + if (relatorKeys) { + for (const key of relatorKeys) { + delete attributes[key as keyof PrimaryType]; + } + } return attributes; }; } else { diff --git a/test/issue-77.test.ts b/test/issue-77.test.ts new file mode 100644 index 0000000..a188575 --- /dev/null +++ b/test/issue-77.test.ts @@ -0,0 +1,41 @@ +import { Relator, Serializer } from "../lib"; + +describe("Issue #77 - Related fields should not appear in attributes", () => { + interface Article { + id: string; + title: string; + } + + interface User { + articles: Article[]; + id: string; + name: string; + } + + const ArticleSerializer = new Serializer
("Article"); + const UserArticleRelator = new Relator( + async (user) => user.articles, + ArticleSerializer + ); + const UserSerializer = new Serializer("User", { + relators: [UserArticleRelator], + }); + + it("should exclude relator fields from attributes by default", async () => { + const user: User = { + id: "1", + name: "Foo", + articles: [ + { id: "1", title: "Article 1" }, + { id: "2", title: "Article 2" }, + ], + }; + + const document = await UserSerializer.serialize(user); + const data = document.data as any; + + expect(data.attributes).not.toHaveProperty("Article"); + expect(data.attributes).toHaveProperty("name", "Foo"); + expect(data.relationships).toHaveProperty("Article"); + }); +}); diff --git a/test/serializer.test.ts b/test/serializer.test.ts index 0db5c27..b8ce13f 100644 --- a/test/serializer.test.ts +++ b/test/serializer.test.ts @@ -273,7 +273,6 @@ describe("Serializer Tests", () => { id: user.id, attributes: { createdAt: user.createdAt.toISOString(), - articles: user.articles, comments: user.comments, }, relationships: { @@ -302,7 +301,6 @@ describe("Serializer Tests", () => { id: user.id, attributes: { createdAt: user.createdAt.toISOString(), - articles: user.articles, comments: user.comments, }, relationships: {