Rails 5 Alpha version / Ruby 2.2.3 / active_model_serializers (0.10.0.rc3) (henceforth referred as AMS)
GIT
remote: https://github.com/rails/rails.git
revision: 5217db2a7acb80b18475709018088535bdec6d30
GEM
remote: https://rubygems.org/
specs:
active_model_serializers (0.10.0.rc3)
I already have a working API-only app utilizing Rabl-Rails to generate the JSON responses.
I am working on making it ready to work with Rails 5 and as part of this evaluating Rails 5 inbuilt API features esp the reusability and flexibility ActiveModel::Serializers can provide.
I created few serializers
- app
- serializers
- client
- base_serializer.rb
- device
- base_serializer.rb
- provider
- base_serializer.rb
The JSON responses in the app is created in composite manner reusing any existing templates and a base_serializer contains basic data about a resource which can be serialized.
Below shown are the 3 serializers I have initially created:
For ActiveRecord model Device
class Device::BaseSerializer < ActiveModel::Serializer
attributes :id, :name
belongs_to :client, serializer: Client::BaseSerializer
def client
unless exclude_client?
object.client
end
end
private
def device_opts?
options.key?(:device) # options inherited from by ActiveModel::Serializer
end
def device_opts
options[:device]
end
def exclude_client?
device_opts? && device_opts.key?(:exclude_client) # options inherited from by ActiveModel::Serializer
end
end
For ActiveRecord model Client
class Client::BaseSerializer < ActiveModel::Serializer
attributes :id, :name, :time_zone
belongs_to :provider, serializer: Provider::BaseSerializer
def provider
unless exclude_provider?
object.provider
end
end
private
def client_opts?
options.key?(:client) # options inherited from by ActiveModel::Serializer
end
def client_opts
options[:client]
end
def exclude_provider?
client_opts? && client_opts.key?(:exclude_provider)
end
end
For ActiveRecord model Provider
class Provider::BaseSerializer < ActiveModel::Serializer
attributes :id, :name
end
Provider model
class Provider < ActiveRecord::Base
has_many :clients, dependent: :destroy
end
Client model
class Client < ActiveRecord::Base
belongs_to :provider
has_many :devices
end
Device model
class Device < ActiveRecord::Base
belongs_to :client
end
Problem
When serializing a Device object its client node doesn't contain the client's provider node as defined by belongs_to :provider, serializer: Provider::BaseSerializer
in Client::BaseSerializer
device = Device.find(1)
options = { serializer: Device::BaseSerializer }
serializable_resource = ActiveModel::SerializableResource.new(device, options)
puts JSON.pretty_generate(serializable_resource.as_json)
{
"id": 1,
"name": "Test Device",
"client": {
"id": 2,
"name": "Test Client",
"time_zone": "Eastern Time (US & Canada)"
}
}
However when serializing a Client object it contains the provider node:
client = Client.find(2)
options = { serializer: Client::BaseSerializer }
serializable_resource = ActiveModel::SerializableResource.new(client, options)
puts JSON.pretty_generate(serializable_resource.as_json)
{
"id": 2,
"name": "Test Client",
"time_zone": "Eastern Time (US & Canada)",
"provider": {
"id": 1,
"name": "Test Provider"
}
}
As can be seen above when we generate a Device's json in its "client" property, client's provider relation is not getting generated. However when we generate a Client's json it contains "provider" property. Passing any include option like following
options = { serializer: Client::BaseSerializer, include: "client.provider.**" }
as mentioned here doesn't have any effect.
I dig into the AMS source code and found that include
option is only considered if the adapter is JSON API
. However, the default adapter is base.config.adapter = :flatten_json
.
As can be seen in the adapter's implementation and ActiveModel::Serializer#attributes(options = {})
method (shown below) only following data is taken into account during serialization:
- Object's attributes
- Object's relationships and their attributes. Relationship's own relations are not taken into account.
ActiveModel::Serializer::Adapter::FlattenJson < ActiveModel::Serializer::Adapter::Json
def serializable_hash(options = nil)
options ||= {}
if serializer.respond_to?(:each)
result = serializer.map { |s| FlattenJson.new(s).serializable_hash(options) }
else
hash = {}
core = cache_check(serializer) do
serializer.attributes(options)
end
serializer.associations.each do |association|
serializer = association.serializer
opts = association.options
if serializer.respond_to?(:each)
array_serializer = serializer
hash[association.key] = array_serializer.map do |item|
cache_check(item) do
item.attributes(opts)
end
end
else
hash[association.key] =
if serializer && serializer.object
cache_check(serializer) do
# As can be seen here ASSOCIATION's serializer's attributes only gets serialize and not its own relations.
serializer.attributes(options)
end
elsif opts[:virtual_value]
opts[:virtual_value]
end
end
end
result = core.merge hash
end
ActiveModel::Serializer
def attributes(options = {})
attributes =
if options[:fields]
self.class._attributes & options[:fields]
else
self.class._attributes.dup # <<<<<<<<<< here
end
attributes.each_with_object({}) do |name, hash|
unless self.class._fragmented
hash[name] = send(name)
else
hash[name] = self.class._fragmented.public_send(name)
end
end
end
As my JSON structures are custom and pre-defined, I cannot switch to JSON API adapter. That leaves the option to use only attributes like
class Client::BaseSerializer < ActiveModel::Serializer
attributes :id, :name, :time_zone
attributes :provider
def provider
unless exclude_provider?
object.provider
end
end
end
But that way I do not get flexibility to use a custom serializer for :provider attribute.
Questions:
Is there any way to get around the problem mentioned above and achieve the desired results?
Is there any provision to ignore an attribute from getting included in the serialized hash? For e.g. using Rabl-Rails in my JSON template I can do following:
node(:client, if: ->(device_decorator) { !device_decorator.exclude_client? } ) do |device_decorator| partial('../clients/base', object: device_decorator.client_decorator) end
With that in place if DeviceDecorator#exclude_client?
returns false, :client node doesn't get generated in the JSON.
Thanks.