A Look Into Ember Data Transforms

Monday, November 23rd, 2015

Ember Data has a feature called transforms that allow you to transform values before they are set on a model or sent back to the server. A transform has two functions: serialize and deserialize. Deserialization converts a value to a format that the client expects. Serialization does the reverse and converts a value to the format expected by the backend. If you’ve been working with Ember Data, then you have already been using transforms and may not have known it. The built in transforms include:

  • string
  • number
  • boolean
  • date

These transforms are used when model attributes are declared using DS.attr(). For example:

// models/person.js
export default DS.Model.extend({
  name: DS.attr('string'),
  age: DS.attr('number'),
  admin: DS.attr('boolean'),
  lastLogin: DS.attr('date'),
  phone: DS.attr()
});

When the model is created, the attributes are transformed to the types specified in the corresponding DS.attr() call. Behind the scenes, each of these DS.attr() calls map to a specific transform class that extends from DS.Transform. If you don’t pass anything to DS.attr(), like the phone attribute in the model above, the value will be passed through:

DS.attr() Transform Class
DS.attr('boolean') DS.BooleanTransform
DS.attr('number') DS.NumberTransform
DS.attr('string') DS.StringTransform
DS.attr('date') DS.DateTransform

So what’s going on behind each of these Transform classes? Let’s take a look at the Ember Data source code.

When you search for NumberTransform, you’ll see it points to this:

ember$data$lib$transforms$base$$default.extend({
  deserialize: function (serialized) {
    var transformed;

    if (ember$data$lib$transforms$number$$empty(serialized)) {
      return null;
    } else {
      transformed = Number(serialized);

      return ember$data$lib$transforms$number$$isNumber(transformed) ? transformed : null;
    }
  },

  serialize: function (deserialized) {
    var transformed;

    if (ember$data$lib$transforms$number$$empty(deserialized)) {
      return null;
    } else {
      transformed = Number(deserialized);

      return ember$data$lib$transforms$number$$isNumber(transformed) ? transformed : null;
    }
  }
});

If you remove the long prefix ember$data$lib$transforms$number$$, the class reads a little easier:

ember$data$lib$transforms$base$$default.extend({
  deserialize: function (serialized) {
    var transformed;

    if (empty(serialized)) {
      return null;
    } else {
      transformed = Number(serialized);

      return isNumber(transformed) ? transformed : null;
    }
  },

  serialize: function (deserialized) {
    var transformed;

    if (empty(deserialized)) {
      return null;
    } else {
      transformed = Number(deserialized);

      return isNumber(transformed) ? transformed : null;
    }
  }
});

You can see that it uses the Number function to convert the value back and forth. If the attribute is not a number, null is returned. StringTransform is similar and pretty self explanatory, using the String function.

ember$data$lib$transforms$base$$default.extend({
  deserialize: function (serialized) {
    return none(serialized) ? null : String(serialized);
  },
  serialize: function (deserialized) {
    return none(deserialized) ? null : String(deserialized);
  }
});

I found the BooleanTransform interesting because it deserializes value types other than Boolean:

  • The strings “true” or “t” in any casing, or “1” will coerce to true, and false otherwise
  • The number 1 will coerce to true, and false otherwise
  • Anything other than boolean, string, or number will coerce to false

Here is the implementation:

ember$data$lib$transforms$base$$default.extend({
  deserialize: function (serialized) {
    var type = typeof serialized;

    if (type === "boolean") {
      return serialized;
    } else if (type === "string") {
      return serialized.match(/^true$|^t$|^1$/i) !== null;
    } else if (type === "number") {
      return serialized === 1;
    } else {
      return false;
    }
  },

  serialize: function (deserialized) {
    return Boolean(deserialized);
  }
});

And lastly, the DateTransform:

ember$data$lib$transforms$base$$default.extend({
  deserialize: function (serialized) {
    var type = typeof serialized;

    if (type === "string") {
      return new Date(Ember.Date.parse(serialized));
    } else if (type === "number") {
      return new Date(serialized);
    } else if (serialized === null || serialized === undefined) {
      // if the value is null return null
      // if the value is not present in the data return undefined
      return serialized;
    } else {
      return null;
    }
  },

  serialize: function (date) {
    if (date instanceof Date) {
      return date.toISOString();
    } else {
      return null;
    }
  }
});

The DateTransform is interesting because it also deserializes a few different values. If the date is a string, it should be in a format recognized by Date.parse(). According to MDN, that date format should be either RFC2822 or ISO 8601.

The ISO 8601 format looks like this: YYYY-MM-DDTHH:mm:ss.sssZ. More information on that can be found here.

Because Date.parse() in some browsers does not support simplified ISO 8601 dates, like Safari 5-, IE 8-, Firefox 3.6-, Ember uses a shim.

Alternatively, a number can be passed that represents the number of milliseconds since 1 January 1970 00:00:00 UTC (Unix Epoch). Otherwise, null or undefined is returned.

The DateTransform serialization process converts it to the ISO 8601 string format if the model property is an instance of Date. Otherwise null is sent.

Creating Custom Transforms

You can also create custom transforms. Here is a simple transform that converts values in cents (maybe the database stores everything in cents) to US dollars.

ember g transform dollars
import DS from 'ember-data';

export default DS.Transform.extend({
  deserialize: function(serialized) {
    return serialized / 100; // returns dollars
  },
  serialize: function(deserialized) {
    return deserialized * 100; // returns cents
  }
});

Then, simply use DS.attr('dollars') in the model:

// models/person.js
export default DS.Model.extend({
  name: DS.attr('string'),
  age: DS.attr('number'),
  admin: DS.attr('boolean'),
  lastLogin: DS.attr('date'),
  phone: DS.attr(),
  spent: DS.attr('dollars')
});

What custom transforms have you made? Thanks for reading!

Interested in learning more about Ember Data and how to use it with any API? Check out my book Ember Data in the Wild - Getting Ember Data to Work With Your API.


Disclaimer: Any viewpoints and opinions expressed in this article are those of David Tang and do not reflect those of my employer or any of my colleagues.

comments powered by Disqus