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),
}
Have in mind that unlike in Active Model Serializers, attributes
in Oj::Serializer
will not take into account methods defined in the serializer.
Specially in the beginning, you can replace attributes
with ams_attributes
to preserve the same behavior.
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 \
def release
album.release_date.strftime('%B %d, %Y')
end, if: -> { album.released? }
end
The shorthand syntax for serializer attributes might not be very palatable at first, but having the entire definition in one place makes it a lot easier to follow, specially 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.