Skip to content

Ember Serializers — Data Format Transformation

DodaTech Updated 2026-06-28 5 min read

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

Ember serializers transform data between the server format and the Ember Data format. They normalize incoming responses into JSON:API format for models, and serialize outgoing requests into the format your API expects.

What You'll Learn

You will learn how serializers work, customize normalization, handle different API formats, serialize relationships, and create per-model serializers.

Why It Matters

Backends rarely return data in the exact format Ember Data expects. Serializers bridge that gap, converting snake_case keys, nested objects, and relationship formats so your models receive clean data.

Real-World Use

A legacy REST API returns data in snake_case with nested objects. A custom serializer converts it to camelCase JSON:API format. When the API migrates to JSON:API, only the serializer changes — all models and templates remain untouched.

flowchart LR
    A[Server Response] --> B[Serializers.normalizeResponse]
    B --> C[JSON:API Format]
    C --> D[Models]
    D --> E[Serializers.serialize]
    E --> F[Server Format]
    F --> G[HTTP Request]

Default Serializer

// app/serializers/application.js
import JSONAPISerializer from '@ember-data/serializer/json-api';

export default class ApplicationSerializer extends JSONAPISerializer {
  // Works with JSON:API backends out of the box
}

Normalizing Non-JSON:API Responses

If your API uses a different format, override normalizeResponse.

// app/serializers/application.js
import JSONSerializer from '@ember-data/serializer/json';

export default class ApplicationSerializer extends JSONSerializer {
  // Works with simpler REST APIs
}

For completely custom formats:

// app/serializers/user.js
import RESTSerializer from '@ember-data/serializer/rest';

export default class UserSerializer extends RESTSerializer {
  normalizeResponse(store, primaryModelClass, payload, id, requestType) {
    // API returns: { status: "ok", data: { user: { id: 1, name: "Alice" }}}
    let transformed = {
      data: {
        id: payload.data.user.id,
        type: 'user',
        attributes: {
          name: payload.data.user.name,
          email: payload.data.user.email
        }
      }
    };

    return super.normalizeResponse(store, primaryModelClass, transformed, id, requestType);
  }
}

Key Customization

Convert between snake_case (API) and camelCase (Ember).

// app/serializers/application.js
import JSONAPISerializer from '@ember-data/serializer/json-api';
import { underscore, camelize } from '@ember/string';

export default class ApplicationSerializer extends JSONAPISerializer {
  // API uses snake_case, Ember uses camelCase
  keyForAttribute(key) {
    return camelize(key);
  }

  keyForRelationship(key) {
    return camelize(key);
  }
}

Serializing for the Server

Override serialize to control how data is sent.

// app/serializers/product.js
import JSONAPISerializer from '@ember-data/serializer/json-api';

export default class ProductSerializer extends JSONAPISerializer {
  serialize(snapshot, options) {
    let json = super.serialize(snapshot, options);
    let data = json.data;

    // Add computed fields
    data.attributes['full_name'] = `${data.attributes['brand']} ${data.attributes['name']}`;

    // Remove internal fields
    delete data.attributes['internal_notes'];

    return json;
  }

  serializeAttribute(snapshot, json, key, attribute) {
    // Only send specific attributes on create
    if (snapshot.record.isNew && key === 'createdAt') {
      return; // Skip sending createdAt on create
    }
    super.serializeAttribute(snapshot, json, key, attribute);
  }
}

Handling Relationships in Serializers

Control how relationships are serialized and normalized.

// app/serializers/post.js
import JSONAPISerializer from '@ember-data/serializer/json-api';

export default class PostSerializer extends JSONAPISerializer {
  // Include specific relationships in serialization
  shouldSerializeHasMany(snapshot, key, relationship) {
    // Only include comments when explicitly requested
    if (key === 'comments') {
      return snapshot.record.hasDirtyAttributes && key === 'comments';
    }
    return true;
  }

  // Custom relationship serialization
  serializeBelongsTo(snapshot, json, relationship) {
    let key = relationship.key;
    let belongsTo = snapshot.belongsTo(key);

    if (belongsTo) {
      // API expects author_id, not relationship object
      json[key + '_id'] = belongsTo.id;
    }
  }
}

Per-Model Serializer Example

// app/serializers/report.js
import RESTSerializer from '@ember-data/serializer/rest';

export default class ReportSerializer extends RESTSerializer {
  // Reports API uses a flat format:
  // { report: { id: 1, title: "...", rows: [...] }}

  normalizeResponse(store, primaryModelClass, payload, id, requestType) {
    let transformed = {
      report: {
        id: payload.report.id,
        title: payload.report.title,
        generatedAt: payload.report.generated_at
      }
    };

    // Handle embedded records
    if (payload.report.rows) {
      transformed.report.rows = payload.report.rows.map(row => {
        return {
          id: row.id,
          type: 'report-row',
          attributes: {
            label: row.label,
            value: row.value,
            trend: row.trend
          }
        };
      });
    }

    return super.normalizeResponse(store, primaryModelClass, transformed, id, requestType);
  }

  serializeIntoHash(hash, typeClass, snapshot, options) {
    // Serialize back to API format
    hash.report = {
      id: snapshot.id,
      title: snapshot.attr('title'),
      generated_at: snapshot.attr('generatedAt')
    };
  }
}

Polymorphic Serialization

// app/serializers/comment.js
import JSONAPISerializer from '@ember-data/serializer/json-api';

export default class CommentSerializer extends JSONAPISerializer {
  serializeBelongsTo(snapshot, json, relationship) {
    let key = relationship.key;

    if (key === 'parent') {
      let parent = snapshot.belongsTo('parent');
      if (parent) {
        // Include type for polymorphic relationship
        json.parent_id = parent.id;
        json.parent_type = parent.modelName;
      }
    } else {
      super.serializeBelongsTo(snapshot, json, relationship);
    }
  }
}

Testing Serializers

// tests/unit/serializers/product-test.js
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';

module('Unit | Serializer | product', function(hooks) {
  setupTest(hooks);

  test('it normalizes API response', function(assert) {
    let store = this.owner.lookup('service:store');
    let serializer = this.owner.lookup('serializer:product');

    let payload = {
      product: {
        id: '1',
        name: 'Widget',
        price_cents: 1999
      }
    };

    let normalized = serializer.normalizeResponse(
      store,
      store.modelFor('product'),
      payload,
      '1',
      'findRecord'
    );

    assert.equal(normalized.data.attributes.name, 'Widget');
    assert.equal(normalized.data.attributes.priceCents, 1999);
  });

  test('it serializes to API format', function(assert) {
    let serializer = this.owner.lookup('serializer:product');
    let snapshot = { /* mock snapshot */ };

    let serialized = serializer.serialize(snapshot);
    assert.ok(serialized.data.attributes);
  });
});

Common Mistakes

  1. Not overriding both normalize and serialize. If you customize one direction, you must customize the other. Inconsistent formats cause bugs.
  2. Modifying payload in place instead of returning new objects. Serializers should not mutate the original payload. Create new objects to avoid side effects.
  3. Forgetting to call super in normalizeResponse. Skipping super breaks the chain. Always call super at the end.
  4. Hardcoding model names in serializers. Use primaryModelClass.modelName instead of hardcoding strings.
  5. Not handling belongsTo in serialization. Omitting relationship serialization causes errors when saving related records.

Practice Questions

  1. What does the normalizeResponse method do?
  2. How do you convert snake_case to camelCase in serializers?
  3. How do you exclude certain attributes from serialization?
  4. What is the difference between JSONAPISerializer and RESTSerializer?
  5. Challenge: Create a serializer for an API that returns: { "status": "success", "data": { "post": { "id": 1, "title": "Hello", "author_name": "Alice", "published_at": "2026-01-15" } } }. Normalize to JSON:API format in Ember. Handle the author_name field (it is not a relationship, just a flat string). Also serialize back to the original format for saves.

FAQ

What is the default serializer in Ember?

JSONAPISerializer. It expects JSON:API format.

Can I use a different serializer for different models?

Yes. Generate per-model serializers.

How do I handle nested JSON responses?

Override normalizeResponse to extract nested data into JSON:API format.

What is `serializeIntoHash` used for?

It controls how the serialized data is placed into the request payload object.

Do serializers affect template data?

Yes. Normalized data flows through serializers before reaching templates.

Mini Project

Create a serializer layer for a multi-format API: (1) Application serializer that converts snake_case to camelCase. (2) A legacy serializer for an old API that returns wrapped responses ({ "data": { "type": "user", ... }}). (3) An external API serializer for a third-party service with a completely different format. (4) Write unit tests for each serializer.

What's Next

Now that you understand serializers, learn Ember Services for shared state. Then explore Ember Controllers for route-specific logic.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro