The DSL of oj_serializers
is meant to be similar to the one provided by active_model_serializers
to make the migration process simple,
though the goal is not to be a drop-in replacement.
To use the same format in controllers, using the root
, serializer
, each_serializer
options, you should require the compatibility layer:
# config/initializers/json.rb
require 'oj_serializers/compat'
Otherwise, use one
and many
to serialize objects or enumerables:
render json: {
favorite: LegacyAlbumSerializer.new(album),
purchases: albums.map { |album| LegacyAlbumSerializer.new(album) },
}
# becomes
render json: {
favorite: AlbumSerializer.one(album),
purchases: AlbumSerializer.many(albums),
}
If you read the Attributes DSL section, you might have noticed that you need
to explicitly tell when a method in the serializer should be used by
specifying it with attribute
.
This makes the serializers more predictable and more maintainable, but it can
make it challenging to migrate from active_model_serializers
.
Specially in the beginning, you can replace attributes
with ams_attributes
to preserve the same behavior.
ams_attributes
works like attributes
in active_model_serializers
: by
calling a method in the serializer if defined, or calling
read_attribute_for_serialization
in the model.
class AlbumSerializer < ActiveModel::Serializer
attributes :name, :release
has_many :songs
def album
object
end
def release
album.release_date.strftime('%B %d, %Y')
end
def include_release?
album.released?
end
end
# becomes
class AlbumSerializer < Oj::Serializer
ams_attributes :name, :release
# The serializer class must be explicitly provided.
has_many :songs, serializer: SongSerializer
def release
album.release_date.strftime('%B %d, %Y')
end
# This AMS magic still works.
def include_release?
album.released?
end
end
Once your serializer is working as expected, you can further refactor it to be more performant by using attributes
and serializer_attributes
.
Being explicit about where the attributes are coming from makes the serializers easier to understand and more maintainable.
class AlbumSerializer < Oj::Serializer
attributes :name
has_many :songs, serializer: SongSerializer
attribute if: -> { album.released? }
def release
album.release_date.strftime('%B %d, %Y')
end
end
The shorthand syntax for serializer attributes might seem odd at first, but it makes it a lot easier to differentiate helper methods from attributes, especially in large serializers.
You can use these serializers inside arrays, hashes, or even inside ActiveModel::Serializer
.
class LegacyAlbumSerializer < ActiveModel::Serializer
attributes :songs
def songs
SongSerializer.many(object.songs)
end
end
As a result, you can gradually replace the serializers one by one as needed.
In case you need to access path helpers in your serializers, you can use the following:
class BaseJsonSerializer < Oj::Serializer
include Rails.application.routes.url_helpers
def default_url_options
Rails.application.routes.default_url_options
end
end
One slight variation that might make it easier to maintain in the long term is
to use a separate singleton service to provide the url helpers and options, and
make it available as urls
.
This pattern is usually a bad practice, because it couples the serializer to the controller, making it harder to reuse or test independently.
However, it can be handy if you were already relying on this with ActiveModel::Serializer
:
class ApplicationController < ActionController::Base
before_action { Thread.current[:current_controller] = self }
end
class BaseJsonSerializer < Oj::Serializer
def scope
@scope ||= Thread.current[:current_controller]
end
def params
@params ||= scope&.params || {}
end
end
Using request_store
or request_store_rails
is advisable instead of using
Thread.current
, since keeping a reference to a controller after the request is
done could cause memory bloat and additional problems.