Search code examples
ember.jsember-data

Properly formatting Ember custom serializer response for sideloaded JSON API data


I want to try serving sideloaded data to my Ember app. I have a city model which hasMany fireStations. I changed my hasMany relationship to have an { async: false } option to coincide with sideloading, since the data will no longer be loaded asynchronously.

I use a custom serializer, and I am logging the response from normalize(). It looks like this for my data.

{
  "data": {
    "id": "3",
    "type": "city",
    "attributes": {
      "name": "Anytown USA"
    },
    "relationships": {
      "fireStations": {
        "data": [
          {
            "id": "17",
            "type": "fire-station"
          },
          {
            "id": "18",
            "type": "fire-station"
          }
        ]
      }
    }
  },
  "included": [
    {
      "id": "17",
      "type": "fire-station",
      "attributes": {
        "name": "North Side Fire Station"
      },
      "relationships": {}
    },
    {
      "id": "18",
      "type": "fire-station",
      "attributes": {
        "name": "East Side Fire Station"
      },
      "relationships": {}
    }
  ]
}

I think my sideloaded data is properly formatted. It seems to match the example in the guides. The included array is populated with all my sideloaded data, and it all seems to be formatted as needed.

However, I'm hitting this error in my app.

Assertion Failed: You looked up the 'fireStations' relationship on a 'city' with id 3 but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record, or specify that the relationship is async ('DS.hasMany({ async: true })')

According to the JSON API spec, the format I am using seems to be correct for loading compound documents.

In a compound document, all included resources MUST be represented as an array of resource objects in a top-level included member.

And it seems links are optional, so it should be fine that I am leaving them out.

The optional links member within each resource object contains links related to the resource.

I can't figure out what the issue is here. I forked ember-data locally and I do indeed see this assertion is triggered.

If I manually loop over the manyArray in has-many.js, I see that each record is marked as isEmpty being true

Why are the has-many records returning isEmpty === true? What might I be doing wrong that is preventing sideloading from working correctly?


Solution

  • I was able to create an Ember Twiddle to find, recreate, and resolve my issue.

    Short answer is that my custom serializer was dropping included during normalization. I was confusing the responsibilities between normalize() and normalizeResponse() and returning included from the wrong method.

    I was trying to set the included property on the hash in normalize() (that's what you see in my question above). This approach was entirely wrong.

    included should be present in the response from normalizeResponse(), if needed. So I (superfluously) had included in response(), but didn't have it where it was actually needed in normalizeResponse().

    normalizeResponse (store, primaryModelClass, payload, id, requestType) {
      let { data, included } = this._super(...arguments);
    
      // Do some manipulation on `data` or `included`.
    
      // Oh no! We lost the `included` array!
      // This will cause an error
      // because we are saying the
      // relationship is not async, but we're
      // dropping the `included` data!
      return { data };
    
      // This is what we want, for this contrived example.
      // return { data, included };
    }
    

    If an included array needs to be returned, it should be returned from normalizeResponse(), not normalize(). This should be obvious to anyone looking at the source code for the JSONAPISerializer in json-api.js, but I somehow got lost along the way.

    In the JSONAPISerializer implementation for ember-data 2.17.0, you can see that included is set on the documentHash in a method invoked by normalizeResponse().

    My serializer does a lot of customization since my API is very non-standard, and I misunderstood some of the details here. Also, I think it's a bit difficult to understand what should and shouldn't be done in each of the various normalize... API hooks, despite the docs. Though, I think if I spent more time reading them, I probably would've understood this.