Repository: cardstack/boxel
Pull request #2430: fix linked cards occasionally missing. Use value.data.id
Author: login: tintinthong
Created: 2025-04-14T12:41:20Z | Merged: 2025-04-21T17:02:16Z

This PR is to fix the issue where linked data is missing 


<img width="909" alt="Screenshot 2025-04-09 at 23 40 10" src="https://github.com/user-attachments/assets/44d13832-9967-4eeb-b4c0-d10e9b0d1e43" />



Diff:
diff --git a/packages/base/card-api.gts b/packages/base/card-api.gts
index 6e4fbf55ce3..4bd6244f169 100644
--- a/packages/base/card-api.gts
+++ b/packages/base/card-api.gts
@@ -39,6 +39,7 @@ import {
   realmURL,
   localId,
   formats,
+  isLocalResourceID,
   type Format,
   type Meta,
   type CardFields,
@@ -377,11 +378,14 @@ function resourceFrom(
   resourceId: string | undefined,
 ): LooseCardResource | undefined {
   if (doc == null) {
-    return undefined;
+    return;
   }
   let data: CardResource[];
   if (isSingleCardDocument(doc)) {
-    if (resourceId == null) {
+    if (resourceId === undefined) {
+      return undefined;
+    }
+    if (resourceId === null) {
       return doc.data;
     }
     data = [doc.data];
@@ -968,6 +972,14 @@ class LinksTo<CardT extends CardDefConstructor> implements Field<CardT> {
         }' cannot deserialize non-relationship value ${JSON.stringify(value)}`,
       );
     }
+    if (Array.isArray(value.data)) {
+      throw new Error(
+        `linksTo field '${this.name}' cannot deserialize a list of resource ids`,
+      );
+    }
+    if (isLocalResourceID(value.data)) {
+      throw new Error(`lid is not supported in deserialization`);
+    }
     if (value?.links?.self == null || value.links.self === '') {
       return null;
     }
@@ -978,8 +990,12 @@ class LinksTo<CardT extends CardDefConstructor> implements Field<CardT> {
       cachedInstance[isSavedInstance] = true;
       return cachedInstance as BaseInstanceType<CardT>;
     }
-    let resourceId = new URL(value.links.self, relativeTo).href;
-    let resource = resourceFrom(doc, resourceId);
+    //links.self is used to tell the consumer of this payload how to get the resource via HTTP. data.id is used to tell the
+    //consumer of this payload how to get the resource from the side loaded included bucket. we need to strictly only
+    //consider data.id when calling the resourceFrom() function (which actually loads the resource out of the included
+    //bucket). we should never used links.self as part of that consideration. If there is a missing data.id in the resource entity
+    //that means that the serialization is incorrect and is not JSON-API compliant.
+    let resource = resourceFrom(doc, value.data?.id);
     if (!resource) {
       if (loadedValue !== undefined) {
         return loadedValue;
@@ -1343,6 +1359,14 @@ class LinksToMany<FieldT extends CardDefConstructor>
             )}`,
           );
         }
+        if (Array.isArray(value.data)) {
+          throw new Error(
+            `linksToMany field '${this.name}' cannot deserialize a list of resource ids`,
+          );
+        }
+        if (isLocalResourceID(value.data)) {
+          throw new Error(`lid is not supported in deserialization`);
+        }
         // TODO support lid (e,g, value.links will not exist in this case)
         if (value.links?.self == null) {
           return null;
@@ -1354,12 +1378,12 @@ class LinksToMany<FieldT extends CardDefConstructor>
           cachedInstance[isSavedInstance] = true;
           return cachedInstance;
         }
-        let resourceId = new URL(
-          value.links.self,
-          !Array.isArray(doc.data) && 'id' in doc.data && doc.data.id
-            ? doc.data.id
-            : relativeTo,
-        ).href;
+        //links.self is used to tell the consumer of this payload how to get the resource via HTTP. data.id is used to tell the
+        //consumer of this payload how to get the resource from the side loaded included bucket. we need to strictly only
+        //consider data.id when calling the resourceFrom() function (which actually loads the resource out of the included
+        //bucket). we should never used links.self as part of that consideration. If there is a missing data.id in the resource entity
+        //that means that the serialization is incorrect and is not JSON-API compliant.
+        let resourceId = value.data?.id;
         if (loadedValues && Array.isArray(loadedValues)) {
           let loadedValue = loadedValues.find(
             (v) => isCardOrField(v) && 'id' in v && v.id === resourceId,
diff --git a/packages/host/tests/integration/components/serialization-test.gts b/packages/host/tests/integration/components/serialization-test.gts
index dc0df417306..2b536004347 100644
--- a/packages/host/tests/integration/components/serialization-test.gts
+++ b/packages/host/tests/integration/components/serialization-test.gts
@@ -1926,11 +1926,19 @@ module('Integration | serialization', function (hooks) {
             links: {
               self: `${testRealmURL}Person/hassan`,
             },
+            data: {
+              id: `${testRealmURL}Person/hassan`,
+              type: 'card',
+            },
           },
           favorite: {
             links: {
               self: `${testRealmURL}Person/hassan`,
             },
+            data: {
+              id: `${testRealmURL}Person/hassan`,
+              type: 'card',
+            },
           },
         },
         meta: {
@@ -3897,16 +3905,28 @@ module('Integration | serialization', function (hooks) {
             links: {
               self: `${testRealmURL}Certificate/0`,
             },
+            data: {
+              id: `${testRealmURL}Certificate/0`,
+              type: 'card',
+            },
           },
           'posts.0.author.certificate': {
             links: {
               self: `${testRealmURL}Certificate/1`,
             },
+            data: {
+              id: `${testRealmURL}Certificate/1`,
+              type: 'card',
+            },
           },
           'posts.1.author.certificate': {
             links: {
               self: `${testRealmURL}Certificate/2`,
             },
+            data: {
+              id: `${testRealmURL}Certificate/2`,
+              type: 'card',
+            },
           },
         },
         meta: {
@@ -4536,11 +4556,19 @@ module('Integration | serialization', function (hooks) {
               links: {
                 self: `${testRealmURL}Pet/mango`,
               },
+              data: {
+                id: `${testRealmURL}Pet/mango`,
+                type: 'card',
+              },
             },
             'pets.1': {
               links: {
                 self: `${testRealmURL}Pet/vanGogh`,
               },
+              data: {
+                id: `${testRealmURL}Pet/vanGogh`,
+                type: 'card',
+              },
             },
           },
           meta: {
@@ -4949,8 +4977,24 @@ module('Integration | serialization', function (hooks) {
             firstName: 'Hassan',
           },
           relationships: {
-            'pets.0': { links: { self: `${testRealmURL}Pet/mango` } },
-            'pets.1': { links: { self: `${testRealmURL}Pet/vanGogh` } },
+            'pets.0': {
+              links: {
+                self: `${testRealmURL}Pet/mango`,
+              },
+              data: {
+                id: `${testRealmURL}Pet/mango`,
+                type: 'card',
+              },
+            },
+            'pets.1': {
+              links: {
+                self: `${testRealmURL}Pet/vanGogh`,
+              },
+              data: {
+                id: `${testRealmURL}Pet/vanGogh`,
+                type: 'card',
+              },
+            },
           },
           meta: {
             adoptsFrom: {
@@ -5614,8 +5658,20 @@ module('Integration | serialization', function (hooks) {
           attributes: { firstName: 'Burcu' },
           relationships: {
             friend: { links: { self: `${testRealmURL}Friend/hassan` } },
-            'friendPets.0': { links: { self: `${testRealmURL}Pet/mango` } },
-            'friendPets.1': { links: { self: `${testRealmURL}Pet/vanGogh` } },
+            'friendPets.0': {
+              links: { self: `${testRealmURL}Pet/mango` },
+              data: {
+                id: `${testRealmURL}Pet/mango`,
+                type: 'card',
+              },
+            },
+            'friendPets.1': {
+              links: { self: `${testRealmURL}Pet/vanGogh` },
+              data: {
+                id: `${testRealmURL}Pet/vanGogh`,
+                type: 'card',
+              },
+            },
           },
           meta: {
             adoptsFrom: { module: `${testRealmURL}test-cards`, name: 'Person' },
diff --git a/packages/runtime-common/card-document.ts b/packages/runtime-common/card-document.ts
index f14820c7ccb..4311b55153e 100644
--- a/packages/runtime-common/card-document.ts
+++ b/packages/runtime-common/card-document.ts
@@ -383,3 +383,12 @@ export function makeCardTypeSummaryDoc(summaries: CardTypeSummary[]) {
 
   return { data };
 }
+
+export function isLocalResourceID(
+  data: ResourceID | null | undefined,
+): data is ResourceLocalID {
+  if (!data) {
+    return false;
+  }
+  return 'lid' in data;
+}
diff --git a/packages/runtime-common/index.ts b/packages/runtime-common/index.ts
index 88f1a08df69..28166329ec7 100644
--- a/packages/runtime-common/index.ts
+++ b/packages/runtime-common/index.ts
@@ -151,6 +151,7 @@ export {
   isCardCollectionDocument,
   isSingleCardDocument,
   isCardDocumentString,
+  isLocalResourceID,
 } from './card-document';
 export { sanitizeHtml } from './dompurify-runtime';
 export { markedSync, markdownToHtml } from './marked-sync';


Reviews:
user: login: habdelra
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T14:24:15Z
user: login: habdelra
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T14:27:57Z
user: login: habdelra
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T14:30:38Z
user: login: habdelra
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T14:32:01Z
user: login: tintinthong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T06:17:32Z
user: login: tintinthong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T06:37:22Z
user: login: habdelra
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T12:19:58Z
user: login: habdelra
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T12:21:23Z
user: login: habdelra
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T12:23:13Z
user: login: tintinthong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T12:23:45Z
user: login: habdelra
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T12:32:43Z
user: login: habdelra
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T12:55:06Z
user: login: habdelra
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T12:55:50Z
user: login: tintinthong
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T12:57:10Z
user: login: habdelra
body: Based on our conversation in discord, the current code shows a misunderstanding of the purpose of `links.self` vs `data.id` in the JSON-API relationship entity.

`links.self` is used to tell the consumer of this payload how to get the resource via HTTP. `data.id` is used to tell the consumer of this payload how to get the resource from the side loaded `included` bucket. we need to strictly only consider `data.id` when calling the `resourceFrom()` function (which actually loads the resource out of the included bucket). we should never used `links.self` as part of that consideration. If there is a missing `data.id` in the resource entity that means that the serialization is incorrect and is not JSON-API compliant.

Let's make sure to add this note above as a comment as well.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T12:59:11Z
user: login: habdelra
body: looks good!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T13:31:25Z

Review comments:
diff_hunk: @@ -966,7 +966,11 @@ class LinksTo<CardT extends CardDefConstructor> implements Field<CardT> {
       cachedInstance[isSavedInstance] = true;
       return cachedInstance as BaseInstanceType<CardT>;
     }
-    let resourceId = new URL(value.links.self, relativeTo).href;
+    let relativeId =
+      value.data && Array.isArray(value.data)
path: packages/base/card-api.gts
user: login: habdelra
body: this is a LinksTo relationship, not a LinksToMany, why would the value here ever be an array? (keep in mind `value` here is the JSONAPI serialization for the relationship).
created_at: 2025-04-14T14:24:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -966,7 +966,11 @@ class LinksTo<CardT extends CardDefConstructor> implements Field<CardT> {
       cachedInstance[isSavedInstance] = true;
       return cachedInstance as BaseInstanceType<CardT>;
     }
-    let resourceId = new URL(value.links.self, relativeTo).href;
+    let relativeId =
+      value.data && Array.isArray(value.data)
+        ? undefined // we don't know the id of the parent
+        : value.data?.id ?? relativeTo;
path: packages/base/card-api.gts
user: login: habdelra
body: generally we strive to serialize with the `links.self` when sending the serialized form of the instance to the server. both `links.self` and `links.data.id` are supported, using both though, can lead to the potential of conflicts if they were ever configured differently from each other. which is why we have opted to pick one (links.self is more concise). i'm super curious what the scenario is when you are deserializing a payload and links.data.id is populated and links.self is not. I think that is the _actual_ bug if this is what is fixing it.
created_at: 2025-04-14T14:27:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -966,7 +966,11 @@ class LinksTo<CardT extends CardDefConstructor> implements Field<CardT> {
       cachedInstance[isSavedInstance] = true;
       return cachedInstance as BaseInstanceType<CardT>;
     }
-    let resourceId = new URL(value.links.self, relativeTo).href;
+    let relativeId =
+      value.data && Array.isArray(value.data)
+        ? undefined // we don't know the id of the parent
+        : value.data?.id ?? relativeTo;
path: packages/base/card-api.gts
user: login: habdelra
body: another thought is to remove links.self altogether and purely only use links.data.id (this this also means we should not serialize this so we don't introduce confusion).
created_at: 2025-04-14T14:30:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -966,7 +966,11 @@ class LinksTo<CardT extends CardDefConstructor> implements Field<CardT> {
       cachedInstance[isSavedInstance] = true;
       return cachedInstance as BaseInstanceType<CardT>;
     }
-    let resourceId = new URL(value.links.self, relativeTo).href;
+    let relativeId =
+      value.data && Array.isArray(value.data)
+        ? undefined // we don't know the id of the parent
+        : value.data?.id ?? relativeTo;
path: packages/base/card-api.gts
user: login: habdelra
body: and another though is to favor links.self, but fallback to links.data.id if links self is not populated and links.data.id is populated. the drawback is that that gets into the confusing place of having these at odds with one another....
created_at: 2025-04-14T14:32:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -966,7 +966,11 @@ class LinksTo<CardT extends CardDefConstructor> implements Field<CardT> {
       cachedInstance[isSavedInstance] = true;
       return cachedInstance as BaseInstanceType<CardT>;
     }
-    let resourceId = new URL(value.links.self, relativeTo).href;
+    let relativeId =
+      value.data && Array.isArray(value.data)
+        ? undefined // we don't know the id of the parent
+        : value.data?.id ?? relativeTo;
path: packages/base/card-api.gts
user: login: tintinthong
body: Thanks for the input. The issue is really that `value.links.self` is relative. In addition, `doc.data.id` seems like the wrong id to be relative to anyway. 

The data values are 

```
value.links.self
../Category/someCategory

doc.data.id
http://test-realm/test/listing

value.data.id
http://test-realm/test/Category/someCategory

resourceId
http://test-realm/Category/someCategory
```

The expected value 

```
resourceId
http://test-realm/test/Category/someCategory
```

I have added the `serialization-test` test "can resolve relative URLs in linksToMany fields irrespective of location of linked cards"



> another thought is to remove links.self altogether and purely only use links.data.id (this this also means we should not serialize this so we don't introduce confusion).

I think value.data.id seems to return an absolute url which is actually what I want. Perhaps, this is what we want to do. 

created_at: 2025-04-15T06:17:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -966,7 +966,11 @@ class LinksTo<CardT extends CardDefConstructor> implements Field<CardT> {
       cachedInstance[isSavedInstance] = true;
       return cachedInstance as BaseInstanceType<CardT>;
     }
-    let resourceId = new URL(value.links.self, relativeTo).href;
+    let relativeId =
+      value.data && Array.isArray(value.data)
+        ? undefined // we don't know the id of the parent
+        : value.data?.id ?? relativeTo;
path: packages/base/card-api.gts
user: login: tintinthong
body: <img width="789" alt="Screenshot 2025-04-15 at 14 35 59" src="https://github.com/user-attachments/assets/2d18b5af-5df1-447a-846c-12ba987c4ec5" />

The JSON API docs consider data to be kinda optional actually. Tho it seems links.self are also treated as optional. I can see why implementation wise it can get unnecessarily complex

I think in deserialization we can prefer value.data.id but I think in general, if we fallback to links.self the deserialization may be wrong

removal of serialization however, lets talk about it perhaps after standup
created_at: 2025-04-15T06:37:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -966,7 +966,11 @@ class LinksTo<CardT extends CardDefConstructor> implements Field<CardT> {
       cachedInstance[isSavedInstance] = true;
       return cachedInstance as BaseInstanceType<CardT>;
     }
-    let resourceId = new URL(value.links.self, relativeTo).href;
+    let relativeId =
+      value.data && Array.isArray(value.data)
+        ? undefined // we don't know the id of the parent
+        : value.data?.id ?? relativeTo;
path: packages/base/card-api.gts
user: login: habdelra
body: Yeah, it’s especially tricky too, because I don’t think you can look at your contained card (or contained contained card, etc as the case may be), to see the id of the outermost document that you are relative too. That would be perfect if you could. 
created_at: 2025-04-15T12:19:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -966,7 +966,11 @@ class LinksTo<CardT extends CardDefConstructor> implements Field<CardT> {
       cachedInstance[isSavedInstance] = true;
       return cachedInstance as BaseInstanceType<CardT>;
     }
-    let resourceId = new URL(value.links.self, relativeTo).href;
+    let relativeId =
+      value.data && Array.isArray(value.data)
+        ? undefined // we don't know the id of the parent
+        : value.data?.id ?? relativeTo;
path: packages/base/card-api.gts
user: login: habdelra
body: Perhaps we need to thread thru a new parameter thru the deserialzation: the id of the document (if there is one), aka “relativeTo” so that we can resolve any relative hrefs. If we did that, there’s about 4-5 places that the relativeTo value would need to be added to function parameters. 
created_at: 2025-04-15T12:21:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -966,7 +966,11 @@ class LinksTo<CardT extends CardDefConstructor> implements Field<CardT> {
       cachedInstance[isSavedInstance] = true;
       return cachedInstance as BaseInstanceType<CardT>;
     }
-    let resourceId = new URL(value.links.self, relativeTo).href;
+    let relativeId =
+      value.data && Array.isArray(value.data)
+        ? undefined // we don't know the id of the parent
+        : value.data?.id ?? relativeTo;
path: packages/base/card-api.gts
user: login: habdelra
body: If you like maybe we can pair on this after standup. 
created_at: 2025-04-15T12:23:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -966,7 +966,11 @@ class LinksTo<CardT extends CardDefConstructor> implements Field<CardT> {
       cachedInstance[isSavedInstance] = true;
       return cachedInstance as BaseInstanceType<CardT>;
     }
-    let resourceId = new URL(value.links.self, relativeTo).href;
+    let relativeId =
+      value.data && Array.isArray(value.data)
+        ? undefined // we don't know the id of the parent
+        : value.data?.id ?? relativeTo;
path: packages/base/card-api.gts
user: login: tintinthong
body: Yes pls that wud be helpful
created_at: 2025-04-15T12:23:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -966,7 +966,11 @@ class LinksTo<CardT extends CardDefConstructor> implements Field<CardT> {
       cachedInstance[isSavedInstance] = true;
       return cachedInstance as BaseInstanceType<CardT>;
     }
-    let resourceId = new URL(value.links.self, relativeTo).href;
+    let relativeId =
+      value.data && Array.isArray(value.data)
+        ? undefined // we don't know the id of the parent
+        : value.data?.id ?? relativeTo;
path: packages/base/card-api.gts
user: login: habdelra
body: I was thinking one argument for not using links.data.id is that it makes it more challenging to copy a set of cards to a different realm. As relative references can be moved more easily. 
created_at: 2025-04-15T12:32:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5004,6 +5004,94 @@ module('Integration | serialization', function (hooks) {
       assert.ok(isSaved(hassan), `${hassan.id} is saved`);
       assert.strictEqual(hassan.firstName, 'Hassan');
     });
+
+    test('can resolve relative URLs in linksToMany fields irrespective of location of linked cards', async function (assert) {
+      class Category extends CardDef {
+        @field name = contains(StringField);
+      }
+      class Listing extends CardDef {
+        @field category = linksToMany(Category);
+      }
+
+      await setupIntegrationTestRealm({
+        loader,
+        mockMatrixUtils,
+        contents: {
+          'test-cards.gts': { Category, Listing },
+        },
+      });
+
+      let someCategory = new Category({ name: 'Sample Category' });
+      await saveCard(
+        someCategory,
+        `${testRealmURL}Category/someCategory`,
+        loader,
+      );
+      let listingDoc: LooseSingleCardDocument = {
+        data: {
+          type: 'card',
+          id: `${testRealmURL}listing`,
+          attributes: {},
+          relationships: {
+            'category.0': {
+              links: {
+                self: '../Category/someCategory',
path: packages/host/tests/integration/components/serialization-test.gts
user: login: habdelra
body: the fact that this is intentionally a bad ref (`..` vs `.`) is good. it proves we are not paying attention to the links.self when loading the data from the included bucket.
created_at: 2025-04-18T12:55:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1322,8 +1322,8 @@ class LinksToMany<FieldT extends CardDefConstructor>
         }
         let resourceId = new URL(
           value.links.self,
-          !Array.isArray(doc.data) && 'id' in doc.data && doc.data.id
-            ? doc.data.id
+          !Array.isArray(value.data) && value.data?.id
+            ? value?.data?.id
path: packages/base/card-api.gts
user: login: habdelra
body: this should just be value?.data?.id. do _not_ consider `links.self` at all.
created_at: 2025-04-18T12:55:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1322,8 +1322,8 @@ class LinksToMany<FieldT extends CardDefConstructor>
         }
         let resourceId = new URL(
           value.links.self,
-          !Array.isArray(doc.data) && 'id' in doc.data && doc.data.id
-            ? doc.data.id
+          !Array.isArray(value.data) && value.data?.id
+            ? value?.data?.id
path: packages/base/card-api.gts
user: login: tintinthong
body: Yes. This is the key ingredient
created_at: 2025-04-18T12:57:10Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-14T12:58:13Z
body: ## Host Test Results
  1 files  ±0    1 suites  ±0   30m 25s ⏱️ -36s
943 tests ±0  938 ✅ ±0  5 💤 ±0  0 ❌ ±0 
950 runs  ±0  945 ✅ ±0  5 💤 ±0  0 ❌ ±0 

Results for commit 0caaaec5. ± Comparison against base commit 4eb88df9.

[test-results]:data:application/gzip;base64,H4sIAIx4BmgC/02MQQ6DIBQFr2JYdwF8aKGXMfDFhFSlQVg1vXuBVHQ58/LmQ2a/uJ08B3YbyJ596jDlaJIPW0XFZTFlS3XVAg4a94xYFahTvfy7qP4YZ+OXImgXLsYQ/ybmrTXlAT0pZDdnsfEl2Pjaw7CuPhUgFI0xDiUHYMKhlcJMFkE5DndFrXxoTa1VjHx/MYk9xAYBAAA=

:recycle: This comment has been updated with latest results.
author_association: CONTRIBUTOR
user: login: habdelra
created_at: 2025-04-18T13:45:48Z
body: So for the LinksTo deserialize, i think the solution would be:
```ts
   let cachedInstance = identityContext.get(
      new URL(value.links.self, relativeTo).href,
    );
    if (cachedInstance) {
      cachedInstance[isSavedInstance] = true;
      return cachedInstance as BaseInstanceType<CardT>;
    }
   /*
links.self is used to tell the consumer of this payload how to get the resource via HTTP. data.id is used to tell the 
consumer of this payload how to get the resource from the side loaded included bucket. we need to strictly only 
consider data.id when calling the resourceFrom() function (which actually loads the resource out of the included 
bucket). we should never used links.self as part of that consideration. If there is a missing data.id in the resource entity 
that means that the serialization is incorrect and is not JSON-API compliant.
    */
    // only this line changes--we don't even need resourceId
    let resource = resourceFrom(doc, value?.data?.id); 
    if (!resource) {
      if (loadedValue !== undefined) {
        return loadedValue;
      }
      return {
        type: 'not-loaded',
        reference: value.links.self,
      };
    }
```

And for LinksToMany the solution would be:
```ts
        let cachedInstance = identityContext.get(
          new URL(value.links.self, relativeTo).href,
        );
        if (cachedInstance) {
          cachedInstance[isSavedInstance] = true;
          return cachedInstance;
        }
   /*
links.self is used to tell the consumer of this payload how to get the resource via HTTP. data.id is used to tell the 
consumer of this payload how to get the resource from the side loaded included bucket. we need to strictly only 
consider data.id when calling the resourceFrom() function (which actually loads the resource out of the included 
bucket). we should never used links.self as part of that consideration. If there is a missing data.id in the resource entity 
that means that the serialization is incorrect and is not JSON-API compliant.
    */
        let resourceId = value?.data?.id;
        if (resourceId) {
          if (loadedValues && Array.isArray(loadedValues)) {
            let loadedValue = loadedValues.find(
              (v) => isCardOrField(v) && 'id' in v && v.id === resourceId,
            );
            if (loadedValue) {
              return loadedValue;
            }
          }
        }
        let resource = resourceId ? resourceFrom(doc, resourceId) : undefined;
        if (!resource) {
          return {
            type: 'not-loaded',
            reference: value.links.self,
          };
        }
```
author_association: CONTRIBUTOR
user: login: tintinthong
created_at: 2025-04-21T10:53:18Z
body: Actually https://github.com/cardstack/boxel/pull/2430/commits/81d88821834a33723f35a16101341623f157f229 test are already covered in serialization test
author_association: CONTRIBUTOR