Skip to content

Ember Models — Attributes, Transforms, and Relationships

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Ember Models. We cover key concepts, practical examples, and best practices to help you master this topic.

Ember models define the structure and behavior of your application data. They specify attribute types, transform values to and from the server format, define relationships between models, and compute derived values through getters and methods.

What You'll Learn

You will learn how to define model attributes with type transforms, establish relationships, create computed properties, and use model lifecycle hooks.

Why It Matters

Well-defined models ensure data consistency across the application. Attribute types enforce data shape. Relationships model real-world connections. Transforms handle Serialization so templates always receive the right format.

Real-World Use

A Compliance tracking system models Audit, Finding, Remediation, and Auditor. A finding belongs to an audit and has many remediations. Each auditor has many audits. This relationship graph powers dashboards, reports, and compliance scores.

flowchart TD
    A[Auditor] -->|hasMany| B[Audit]
    B -->|hasMany| C[Finding]
    C -->|hasMany| D[Remediation]
    D -->|belongsTo| C
    D -->|belongsTo| E[Assignee]

Defining Attributes

Use @attr with a type transform.

// app/models/product.js
import Model, { attr } from '@ember-data/model';

export default class ProductModel extends Model {
  // Basic types
  @attr('string') name;
  @attr('number') price;
  @attr('boolean') inStock;
  @attr('date') releasedAt;

  // No type — stored as-is
  @attr() metadata;

  // Custom transform
  @attr('currency') cost;

  // Computed from attributes
  get formattedPrice() {
    return `$${this.price.toFixed(2)}`;
  }

  get isNew() {
    let daysOld = (Date.now() - this.releasedAt) / (1000 * 60 * 60 * 24);
    return daysOld < 30;
  }
}

Built-in Transforms

Ember Data includes standard transforms: string, number, boolean, date.

// String transform always returns a string
@attr('string') title;  // null becomes ''

// Number transform converts to number
@attr('number') count;   // '42' becomes 42

// Boolean transform converts to true/false
@attr('boolean') active; // 'true' becomes true

// Date transform converts to Date object
@attr('date') createdAt;  // ISO string becomes Date

// Without type — raw value
@attr() tags;  // Stored as-is

Custom Transforms

Create transforms for domain-specific types.

ember generate transform currency
// app/transforms/currency.js
import Transform from '@ember-data/serializer/transform';

export default class CurrencyTransform extends Transform {
  deserialize(serialized) {
    // Server sends cents (12345) → display as dollars ($123.45)
    if (serialized == null) return 0;
    return serialized / 100;
  }

  serialize(deserialized) {
    // Display dollars ($123.45) → server expects cents (12345)
    if (deserialized == null) return 0;
    return Math.round(deserialized * 100);
  }
}
// Usage in model
@attr('currency') price;  // Server: 1999 → Client: 19.99

One-to-Many: hasMany

// app/models/category.js
import Model, { attr, hasMany } from '@ember-data/model';

export default class CategoryModel extends Model {
  @attr('string') name;
  @attr('string') slug;
  @attr('text') description;

  @hasMany('product') products;

  get productCount() {
    return this.products.length;
  }
}

Many-to-One: belongsTo

// app/models/product.js
import Model, { attr, belongsTo, hasMany } from '@ember-data/model';

export default class ProductModel extends Model {
  @attr('string') name;
  @attr('number') price;
  @attr('number') stock;

  @belongsTo('category') category;
  @hasMany('review') reviews;
}

Many-to-Many

Use hasMany on both sides:

// app/models/post.js
import Model, { attr, hasMany } from '@ember-data/model';

export default class PostModel extends Model {
  @attr('string') title;
  @hasMany('tag') tags;
}

// app/models/tag.js
import Model, { attr, hasMany } from '@ember-data/model';

export default class TagModel extends Model {
  @attr('string') name;
  @hasMany('post') posts;
}

Polymorphic Relationships

For models that can belong to different types:

// app/models/comment.js
import Model, { attr, belongsTo } from '@ember-data/model';

export default class CommentModel extends Model {
  @attr('string') text;
  @attr('date') createdAt;

  // Polymorphic — can belong to a Post or a Video
  @belongsTo('commentable', { polymorphic: true }) parent;
}

// app/models/post.js
import Model, { attr, hasMany } from '@ember-data/model';

export default class PostModel extends Model {
  @attr('string') title;
  @hasMany('comment') comments;
}

// app/models/video.js
import Model, { attr, hasMany } from '@ember-data/model';

export default class VideoModel extends Model {
  @attr('string') title;
  @attr('number') duration;
  @hasMany('comment') comments;
}

Model Lifecycle Hooks

// app/models/invoice.js
import Model, { attr, hasMany } from '@ember-data/model';

export default class InvoiceModel extends Model {
  @attr('string') number;
  @attr('date') issuedAt;
  @attr('string') status;
  @hasMany('line-item') lineItems;

  // Called when record is created
  init() {
    super.init(...arguments);
    if (!this.issuedAt) {
      this.issuedAt = new Date();
    }
  }

  // Called after record is loaded from server
  ready() {
    super.ready();
    console.log(`Invoice #${this.number} loaded`);
  }

  // Called when attributes become dirty
  becameDirty() {
    console.log('Invoice has unsaved changes');
  }

  get total() {
    return this.lineItems.reduce((sum, item) => sum + item.total, 0);
  }

  get isOverdue() {
    return this.status === 'issued' &&
           this.issuedAt < new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
  }
}
// Creating a product with category
let category = this.store.peekRecord('category', 1);
let product = this.store.createRecord('product', {
  name: 'New Product',
  price: 29.99,
  category: category  // Set relationship
});
await product.save();

// Add to existing relationship
let post = await this.store.findRecord('post', 1);
let comment = this.store.createRecord('comment', {
  text: 'Great post!',
  post: post
});
await comment.save();
// Or await post.comments.pushObject(comment) then post.save()

Common Mistakes

  1. Using attr() without a type parameter. Without a type, no transformation happens. Always specify string, number, boolean, or date.
  2. Mutating hasMany arrays directly. Use pushObject, removeObject, or set the IDs via store.createRecord. Direct mutation breaks tracking.
  3. Forgetting async: false on relationships. By default, relationships are async. Access returns a promise. Add async: false for sync loading.
  4. Over-saving with circular relationships. Saving a model that belongs to another can trigger cascading saves. Control this with { save: false } on the relationship.
  5. Not using init() for defaults. Default attribute values go in init() not in the class body. Class body defaults are not reactive.

Practice Questions

  1. What types does Ember Data provide for attributes?
  2. How do you create a custom transform?
  3. What is the difference between belongsTo and hasMany?
  4. How do you handle polymorphic relationships?
  5. Challenge: Design models for a project management app: Project (hasMany tasks, belongsTo manager), Task (belongsTo project, hasMany comments, belongsTo assignee), User (hasMany tasks, hasMany managedProjects), Comment (belongsTo task, belongsTo author). Include custom transforms for priority (enum) and status (enum). Add computed properties for task completion percentage per project.

FAQ

What does the `date` transform do?

It converts ISO 8601 strings to JavaScript Date objects on deserialize and back to ISO strings on serialize.

Can I have optional attributes?

All attributes are optional. They default to null when undefined.

How do I set default values?

Use init(): init() { super.init(...arguments); if (!this.title) this.title = 'Untitled'; }

What is the `async` option on relationships?

If async: true (default), related records load asynchronously via a promise. If false, they load synchronously from cache.

Can I use native ES getters in models?

Yes. Getters are recommended over computed() for derived data.

Mini Project

Create a complete model layer for a library management system: Book (title, author, ISBN, publishedYear, pages, genre), Member (name, email, joinDate, membershipType), Loan (book, member, borrowedAt, dueAt, returnedAt), Genre (name, description, hasMany books). Include custom transforms for ISBN validation and membership type enum. Add computed: isOverdue, daysRemaining, activeLoans.

What's Next

Now that you understand models, learn Ember Adapters for API configuration. Then explore Ember Serializers for data format handling.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro