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
toset_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 insteadattribute :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 blockattribute :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 ofAttachmentSerializer
. - 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 theActiveModelSerializers::Adapter:JsonApi:Link
class. In fast_jsonapilink
blocks work the same way as attribute blocks, acceptingobject
andparams
as arguments.
Working with ID
- JsonAPI standard defines that
id
(andtype
) fields live outside ofattributes
hash, so why do we lump it with other attributes? As part of the migration we need to removeid
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 addset_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
andEventFullSerializer < EventSerializer
, now you will have to add declarations such asset_type
andset_id
toEventFullSerializer
even if they are declared onEventSerializer
- 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 theincluded
hash. 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.