Active_model_serializers: How to customize a hash serialization?

Created on 8 Jan 2013  路  7Comments  路  Source: rails-api/active_model_serializers

Hi,

I can't find a good way to do a custom serialization of a plain old Hash.

Here is an example :

class MyObject
  include ActiveModel::SerializerSupport

  attr_reader :name, :age, :address, :settings

  def initialize(name, age, address, settings = {})
    @name = name
    @age = age
    @address = address
    @settings = settings.merge(:timezone => 'GMT', :other => 'xyz')
  end

end

class MyObjectSerializer < ActiveModel::Serializer
  attributes :name, :age, :settings
end

MyObjectSerializer.new(MyObject.new('toto', 12, 'mars', {:timezone => 'Paris', :foo => 'bar'})).as_json

# => {:my_object=>{:name=>"toto", :age=>12, :settings=>{:timezone=>"GMT", :foo=>"bar", :other=>"xyz"}}}

I'd like not to serialize all the key/values of the internal settings Hash, so I make a custom Serializer

class MyObjectSerializer < ActiveModel::Serializer
  attributes :name, :age, :settings

  def settings
    SettingsSerializer.new(object.settings).as_json
  end
end

class SettingsSerializer < ActiveModel::Serializer
  attributes :timezone
end

But I get an error : NoMethodError: undefined methodread_attribute_for_serialization' for {:timezone=>"GMT", :foo=>"bar", :other=>"xyz"}:Hash`

So I have a few bad choices (as far as I can tell) :

  1. I can add a custom SettingsSerializer#attributes method with only the keys I want, but the natural DSL of AMS (using attributes in the class definition) is useless
  2. I can make a value object to embed the hash and use a regular serializer but it seems a bit overkill
  3. I can just use a hash which I delete the undesired keys from (for example with Hash#only(*keys) but I don't take advantage of the power of AMS.

Is there a proper/clean way to do this or not yet?

Thanks

Documentation Feature 0.9.x

Most helpful comment

I have found an easy way to make serializer for hash:

class LocationSerializer < ActiveModel::Serializer
  attributes :lat, :lon, :city
  attribute :address do
    # Example some custom key 
  end

  def read_attribute_for_serialization(attr)
    object[attr.to_s]
  end
end

My hash example {'lat' => 53.3, 'lon' => 0.19, 'city' => 'London', ...}
I can even use my LocationSerializer through has_one on regular serializer, works good.
[for v10.6]

All 7 comments

A Hash doesn't implement ActiveModel's interface, right? So no, I don't think there's a super straightforward way of doing this yet; we're not serializing Just Any Old Object.

Since this isn't a bug, I'm going to give it a close, but if we want to talk about supporting non-ActiveModel objects, we can talk about it over on the mailing list: https://groups.google.com/forum/?fromgroups#!forum/rails-api-core

Thanks @steveklabnik for that fast answer.

Any time. :)

What about something like this? It serializes Hash with keys provided object key method or it uses hash key when no key option is given.

module ActiveModel

  # Example usage:
  #
  # relations = Site.first.site_user_relations
  # options = {
  #   each_serializer: SiteUserRelationShortSerializer,
  #   key: :site_id
  # }
  # serializer = ActiveModel::HashSerializer.new(relations, options)
  # puts serializer.as_json
  #
  class HashSerializer < Serializer

    def attributes
      attributes = {}
      object.each_pair do |hash_key, item|
        if options[:key]
          if item.respond_to?(options[:key])
            key = item.send(options[:key])
          elsif item.respond_to?(:key?) and item.key?(options[:key])
            key = item[options[:key]]
          else
            fail("Wrong key #{options[:key]} for hash serialization.")
          end
        else
          key = hash_key
        end
        attributes[key] = serialize_item(item)
      end
      attributes
    end

  private

    def serialize_item(item)
      if @options.has_key? :each_serializer
        serializer = @options[:each_serializer]
      elsif item.respond_to?(:active_model_serializer)
        serializer = item.active_model_serializer
      end

      serializable = serializer ? serializer.new(item, @options) : DefaultSerializer.new(item, @options)

      if serializable.respond_to?(:serializable_hash)
        serializable.serializable_hash
      else
        serializable.as_json
      end
    end
  end

end

In case anyone finds this via google I have written a SO answer over here that explains how to use PORO hashes with AMS: http://stackoverflow.com/a/34056668/511168

@therealjessesanford This is fixed in 0.10. I'll reply in SO.

I have found an easy way to make serializer for hash:

class LocationSerializer < ActiveModel::Serializer
  attributes :lat, :lon, :city
  attribute :address do
    # Example some custom key 
  end

  def read_attribute_for_serialization(attr)
    object[attr.to_s]
  end
end

My hash example {'lat' => 53.3, 'lon' => 0.19, 'city' => 'London', ...}
I can even use my LocationSerializer through has_one on regular serializer, works good.
[for v10.6]

Was this page helpful?
0 / 5 - 0 ratings