Last week I started playing around using EmberJS with an Asp.Net Web API backend. And one of the first issues I ran into was getting Ember and Asp Web API to actually communicate with other.

But the first time I tried to retrieve data from the server, Ember Data threw the following exception:

Error while processing route: posts Assertion Failed: You must include an `id` for App.Product in an object passed to `push`

What was going on? After some quick reading through the Ember Data documentation, I realized Ember Data expects the JSON data returned from the server to be in a different format than Asp.Net Web API was returning.

Ember Data expects the JSON data to and from the server to look like this:

{
  posts: [{
    id: 1,
    title: 'First Post'
    publishDate: '2014-11-07'
  }, {
    id: 2,
    title: 'Second Post'
    publishDate: '2014-11-14'
  }]
}

But Asp.Net Web API expects the JSON data to look like this:

[
  {
    Id: 1,
    Title: 'First Post',
    PublishDate: '2014-11-07'
  }, {
    Id: 2,
    Title: 'Second Post',
    PublishDate: '2014-11-14'
  }
]

Notice there are two main differences here between the data formats. First, Ember Data expects a root node named posts to contain the array of posts. But the Web API just returns the array of posts. And second, Ember Data expects the property names to be camelCased. But the Web API has PascalCased property names.

Fortunately Ember Data makes it easy to massage the data that’s coming from and being sent to the server. This means you don’t have to change the default way that Asp.Net Web API serves data.

Massage the data with Serializers

Ember Data uses objects called Serializers to massage and map the data format from the server to the format Ember expects (and vice versa).

Ember includes a few default Serializers for you in its framework that you can choose to use. There’s the JSONSerializer and also the RESTSerializer that extends the JSONSerializer. The RESTSerializer provides most of the mapping we already need. So we will extend it and will only need override a couple functions to perform our specific mapping.

Let’s first map the data coming from the server into the format Ember expects. We will do this by overriding the extract method.

App.ApplicationSerializer = DS.RESTSerializer.extend({
  extract: function(store, primaryType, payload, id, requestType) {
    ...
  }
});

You can see there are quite a few arguments for the extract method, but we are only concerned about two of them: the payload and the primaryType. The payload contains the raw response from the server, and the primaryType will tell us the name of the root node to include in the result.

Let’s first convert the PascalCased property names from the server to camelCased names that Ember expects.

extract: function(store, primaryType, payload, id, requestType) {
  var i, record, propertyName, value, newPropertyName;
  for (i = 0; i < payload.length; i++) {
    record = payload[i];
    for (propertyName in record) {
      value = record[propertyName];
      newPropertyName = propertyName.camelize();
      record[newPropertyName] = value;
      delete record[propertyName];
    }
  }
}

Notice we’re taking advantage of the camelize() function that Ember adds to all string objects. This function takes care of renaming the property to camelCase. So PublishedDate becomes publishedDate with the camelize() function.

Now we need to add the root element to the payload that Ember is expecting.

{
  //root node
  posts: [{...}]
}

And that means we need to retrieve the name for the root element. We can get that information from the primaryType argument. It has a property on it called typeKey that holds the value for the expected root element name.

extract: function(store, primaryType, payload, id, requestType) {
  //... previous code ...
  payloadWithRoot = {}
  payloadWithRoot[primaryType.typeKey] = payload

  this._super(store, primaryType, payloadWithRoot, id, requestType);
}

And the last thing we do is call the _super function to allow the default RESTSerializer to continue on with the normal extraction.

Before we move on, let’s refactor the code so it works with both arrays and single objects that are returned from the server.

App.ApplicationSerializer = DS.RESTSerializer.extend({
  extract: function(store, primaryType, payload, id, requestType) {
    var i, record, payloadWithRoot;
    // if the payload has a length property, then we know it's an array
    if (payload.length) {
      for (i = 0; i < payload.length; i++) {
        record = payload[i];
        this.mapRecord(record);
      }
    } else {
      // payload is a single object instead of an array
      this.mapRecord(payload);
    }
    payloadWithRoot = {}
    payloadWithRoot[primaryType.typeKey] = payload
    this._super(store, primaryType, payloadWithRoot, id, requestType);
  },
  mapRecord: function(record) {
    var propertyName, value, newPropertyName;
    for (propertyName in record.toJSON()) {
      value = item[propertyName];
      newPropertyName = propertyName.camelize();
      item[newPropertyName] = value;
      delete item[propertyName];
    }
  }
});

That’s all it takes to massage the data coming from the server. Now we need to massage the data that is sent down to the server from Ember. And fortunately this is much easier than the other way around.

To do this, we will override the serializeIntoHash function.

serializeIntoHash takes four arguments, but we’re only concerned about two of them. The record argument is the model that is to be saved to the database, and the hash argument is the actual JSON object that will be sent to the server.

So all we need to do is loop through the properties and values in the record object and copy them over to the hash object. And during the copy, we’ll transform the property name to PascalCase.

App.ApplicationSerializer = DS.RESTSerializer.extend({
  extract: function(store, primaryType, payload, id, requestType) {
    ...
  },
  mapRecord: function(record) {
    ...
  },
  serializeIntoHash: function(hash, type, record, options) {
    var jsonRecord, propertyName, value;
    jsonRecord = record.toJSON();
    for (propertyName in jsonRecord) {
      value = jsonRecord[propertyName];
      hash[propertyName.capitalize()] = value;
    }
  }
});

And as you can see, the Ember framework provides the capitalize() function on string objects, and it will take care of transforming the property names to PascalCase.

Conclusion

Ember Data has a bit of a learning curve to understand it. But I really appreciate the amount of thought that went into its design so that it can provide the needed flexibility to work with multiple server backends, including Asp.Net Web API.

Ember Data has a bit of a learning curve to understand it at first. But I really appreciate the amount of thought that went into its design. This is evident in how easy it is to work with multiple API backends, including Asp.Net Web API.

Complete Solution

Here’s the complete solution as a reference.

App.ApplicationSerializer = DS.RESTSerializer.extend({
  extract: function(store, primaryType, payload, id, requestType) {
    var i, record, payloadWithRoot;
    // if the payload has a length property, then we know it's an array
    if (payload.length) {
      for (i = 0; i < payload.length; i++) {
        record = payload[i];
        this.mapRecord(record);
      }
    } else {
      // payload is a single object instead of an array
      this.mapRecord(payload);
    }
    payloadWithRoot = {}
    payloadWithRoot[primaryType.typeKey] = payload
    this._super(store, primaryType, payloadWithRoot, id, requestType);
  },
  mapRecord: function(record) {
    var propertyName, value, newPropertyName;
    for (propertyName in record.toJSON()) {
      value = item[propertyName];
      newPropertyName = propertyName.camelize();
      item[newPropertyName] = value;
      delete item[propertyName];
    }
  },
  serializeIntoHash: function(hash, type, record, options) {
    var jsonRecord, propertyName, value;
    jsonRecord = record.toJSON();
    for (propertyName in jsonRecord) {
      value = jsonRecord[propertyName];
      hash[propertyName.capitalize()] = value;
    }
  }
});

Navigation