Moving from ActiveModel::Serializers to FastJsonAPI

I have used ActiveModel::Serializers for serializing data for JSON APIs for over a year now. I wasn’t happy with it. The performance was mediocre at best and the frequent breaking changes between versions meant that upgrading to newer ones was painful. So when Netflix open-sourced fast_jsonapi with its promise of superior performance and clean codebase, I was ready move on. Over the week I migrated our API code to fast_jsonapi. It was a pretty smooth ride overall, with a few caveats. Here are some of the things worth knowing before making the plunge.

Before:

class MyDocumentSerializer < ActiveModel::Serializer
  type 'document'
  attributes :id, :document_name, :open_permitted
  belongs_to :owner
  has_many :attachments, serializer: MyAttachmentSerializer
  has_one :author

  def open_permitted
    object.open_permitted?(@instance_options[:current_user])
  end

  def id
    "#{SecureRandom.uuid}-#{object.id}"
  end

  link(:self) { api_document_url(object) }
end

class MyDocument < ApplicationRecord
 ...
end

After:

class MyDocumentSerializer
  include FastJsonapi::ObjectSerializer
  set_type :document
  set_id :serialize_id
  attributes :document_name, :open_permitted
  belongs_to :owner, polymorphic: true
  has_many :attachments, serializer: :my_attachment
  has_one :author, record_type: :user

  attribute :open_permitted do |object, params|
    object.open_permitted?(params[:current_user])
  end

  link(:self) { |object, params| api_document_url(object) }
end

class MyDocument < ApplicationRecord
  def serialize_id
    "#{SecureRandom.uuid}-#{object.id}"
  end
end

The Obvious Changes

  • Adding include FastJsonapi::ObjectSerializer  to the serializer and removing < ActiveModel::Serializer
  • Changing type to set_type for setting the type of the serialized record

Attributes

  • Previously, if you wanted to adjust an attribute before serializing it, you defined an instance method in the serializer:  def open_permitted ... end . Now, you follow attribute definition with a block instead attribute :open_permitted do |object| ... end
  • If your custom attribute needed an external param, it was accessible to you through @instance_options . Now, you get it as a second argument in the attribute block attribute :open_permitted do |object, params| ... end
  • To pass external parameters to your serializer, you used to pass them to render: render json: my_document, current_user: current_user . Now you do it differently:MyDocumentSerializer.new(my_document, { params: { current_user: current_user}})
  • If relationship name differs from its type, we need to add record_type definition, like this: has_one :author, record_type: :user
  • If we want to use a different serializer from the one implied by convention, we declare it this way: has_many :attachments, serializer: :my_attachment . As a result, MyAttachmentSerializer  would be used to parse my_attachment, instead of AttachmentSerializer .
  • Previously polymorphic attributes worked out of the box. Now, if you have a relationship which can be of several types, you need to declare this explicitly: belongs_to :owner, polymorphic: true
  • AMS didn’t allow passing external parameters to link blocks, so to make it work, I had to monkey-patch the ActiveModelSerializers::Adapter:JsonApi:Link class. In fast_jsonapi link blocks work the same way as attribute blocks, accepting object and params as arguments.

Working with ID

  • JsonAPI standard defines that id (and type) fields live outside of attributes hash, so why do we lump it with other attributes? As part of the migration we need to remove id from the list of attributes
  • If we need to customize id before serialization, instead of defining an id method on the serializer, we add a new method on the object itself, in the example above def serialize_id ... end, and in the serializer add set_id :serialize_id

Gotchas

  • Any helper methods that were previously defined in the serializer won’t be accessible anymore from the attribute blocks, because inside a block self is set to serializer class, not instance. As such, you need to either turn these methods to class methods, or refactor and move their functionality somewhere else.
  • If you had two serializers, one inheriting from the other, say EventSerializer and EventFullSerializer < EventSerializer, now you will have to add declarations such as set_type and set_id to EventFullSerializer even if they are declared on EventSerializer
  • In the latest 1.3 version of fast_jsonapi, polymorphic relationships don’t support includes. In the above example, owner object won’t appear in the includedhash. The good news is, there is already a commit fixing this. If you aren’t afraid of living on the edge and need this functionality, point the gem to the dev branch:
    gem 'fast_jsonapi', github: 'Netflix/fast_jsonapi', branch: 'dev'

Bottom Line

In short, fast_jsonapi is much more straightforward to use than AMS. While it’s less ambitious – it’s geared only towards jsonapi serialization instead of supporting many different types of serialization, it does what it should do better. You don’t need config files and monkey-patching to make it work. Given that AMS’s near future is shrouded in uncertainty, the switch to fast_jsonapi should be an easy decision.