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::ObjectSerializerto the serializer and removing< ActiveModel::Serializer - Changing
typetoset_typefor 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,MyAttachmentSerializerwould 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
linkblocks, so to make it work, I had to monkey-patch theActiveModelSerializers::Adapter:JsonApi:Linkclass. In fast_jsonapilinkblocks work the same way as attribute blocks, acceptingobjectandparamsas arguments.
Working with ID
- JsonAPI standard defines that
id(andtype) fields live outside ofattributeshash, so why do we lump it with other attributes? As part of the migration we need to removeidfrom 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
selfis 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
EventSerializerandEventFullSerializer < EventSerializer, now you will have to add declarations such asset_typeandset_idtoEventFullSerializereven if they are declared onEventSerializer - In the latest 1.3 version of fast_jsonapi, polymorphic relationships don’t support includes. In the above example,
ownerobject won’t appear in theincludedhash. 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.