36

I have a model object that is not descended from ActiveRecord::Base and is not stored in the database. I created a serializer for it (with the same name + "Serializer"), and in my controller I'm calling render json: object_instance.

The result is an exception from deep inside of render.

I implemented an as_json method that instantiates the serializer and calls it, but the result is then a missing method in the object, read_attribute_for_serialization.

I want my object to act like an ActiveModel-compatible object at least as far as Active Model Serializer goes, but I don't see any reference to this in their documentation.

Robin Daugherty
  • 7,115
  • 4
  • 45
  • 59

6 Answers6

59

The model object needs to include it thusly:

# active_model_serializers 0.10+
class ModelName
  include ActiveModel::Serialization
end

# active_model_serializers < 0.10
class ModelName
  include ActiveModel::SerializerSupport
end

This implements the methods needed within the object, and also auto-discovers the serializer matching the object name, so it can be used transparently just like an ActiveRecord object.

This works with active_model_serializers version 0.8.1 through at least 0.9.3.

Robin Daugherty
  • 7,115
  • 4
  • 45
  • 59
  • Hey Robin can you include an example of your object for people who want to do something similar from scratch? (that is a non active record object that includes the serializer support) – Will May 14 '15 at 21:21
  • did you manage to serialize an array of nested serialized objects inside the Model? – Chris Nov 12 '15 at 21:11
  • Chris, can you open a new question for that one, and include your code? I might be able to answer it, but I need some more detail. – Robin Daugherty Nov 13 '15 at 22:20
13

Following on from mutexkid answer, with active_model_serializers 0.10.0.rc4, the following is required to make a plain old ruby object play nice with a serializer:

PORO:

class SearchResult
  include ActiveModel::Serialization

  attr_reader :stories, :users, :friends

  def initialize(search:)
    @stories = search.stories
    @users = search.users
    @friends = search.friends
  end
end

Serializer:

class SearchResultSerializer < ActiveModel::Serializer
  attributes :stories, :users, :friends
end
b73
  • 1,377
  • 2
  • 14
  • 25
  • This was using Rails 4.0.13 – b73 Mar 11 '16 at 13:32
  • 1
    This is also documented (could be more obvious) and linted in the 0.10. series: https://github.com/rails-api/active_model_serializers/blob/e1d1a3dbf969cfa48d91e978c8c7fd7e2f098e7c/docs/general/rendering.md#serializing-non-activerecord-objects https://github.com/rails-api/active_model_serializers/blob/e1d1a3dbf969cfa48d91e978c8c7fd7e2f098e7c/docs/ARCHITECTURE.md#what-does-a-serializable-resource-look-like – BF4 Mar 21 '16 at 06:03
  • Works nicely with Rails 5.0.1 and AMS 0.10.4 – Brian Sigafoos Feb 24 '17 at 15:30
7

I ran into this yesterday while upgrading to Rails 4.2. ActiveModel::SerializerSupport has been removed for 0.10-stable. I ended up just adding an alias to my POROs which seemed to do the trick:

  alias :read_attribute_for_serialization :send

(active_model_serializers 0.10.0.rc2)

You will also need to add include ActiveModel::Model in your class.

Karl Wilbur
  • 5,898
  • 3
  • 44
  • 54
6

as of active_model_serializers 0.10.x and Rails 4.2, here's what's required to use a plain old ruby object with active model serializers:

Plain old ruby object:

class SearchResult
  extend ActiveModel::Naming
  include ActiveModel::Serialization
  attr_accessor :stories, :users, :friends

  def id
    hash 
  end

  def initialize(search:)
    @stories = search.stories
    @users = search.users
    @friends = search.friends
  end

  def attributes
    { stories: stories,
      users: users,
      friends: friends
    }
  end
end

Serializer:

class SearchResultSerializer < ActiveModel::Serializer
  attributes :stories, :users, :friends
end
mutexkid
  • 1,076
  • 1
  • 12
  • 12
  • with `active_model_serializers 0.10.0.rc4: I found a lot of this setup unnecessary, see my answer below. I have added a seperate answer as comments don't handle code blocks too nicely: – b73 Mar 11 '16 at 10:58
  • This solution worked for me in Rails 5.0.6 - 1) extend ActiveModel::Naming 2) include ActiveModel::Serialization 3) implement `def attributes` – Peter P. Jun 12 '18 at 18:14
5

My account is too new to comment on Karl Wilbur's answer, but heres the code I added so I could pass in a hash object to serialize:

class SerializableHash < Hash
  alias :read_attribute_for_serialization :[]
end

Usage:

  o = SerializableHash.new(user: current_user, account: current_account)
  respond_with(o, serializer: SettingsSerializer)
Supamiu
  • 8,501
  • 7
  • 42
  • 76
ratbeard
  • 197
  • 2
  • 6
0

Expanding upon @ratbeard's answer the following works with any active_model_serializer version 0.9.x and above. I included the minimum amount of active_support and active_model classes needed to get the PORO serializable hash object to work with activemodel serializers outside of rails. For my purposes as an optimization I pass in a pre-computed serialized array of objects that is computed elsewhere in the app. Instead of re-computing you can overload the initializer in the ActiveModel::Serializer subclass to pass it on if it's available.

lib/serializable_hash.rb

class SerializableHash < Hash
  alias :read_attribute_for_serialization :[]
end

app/serializers/email/foo_serializer.rb

require 'active_model/serializer'
require 'active_support/core_ext/object'
require 'active_support/inflector'
require 'active_support/notifications'
class Email::FooSerializer < ActiveModel::Serializer
  attributes :id, :bars, :created_at, :updated_at

  def initialize(foo, precomputed_serialized_array_of_bars =nil)
    unless serialized_bars
      serialized_bars = {}
      foo.bar.each do | bar |
        serialized_searches[bar.baz] ||= []
        serialized_searches[bar.baz] << Email::BarSerializer.new(bar).attributes
      end
    end

    super(SerializableHash[
      id: foo.id,
      bars: serialized_bars,
      created_at: foo.created_at
      updated_at: foo.updated_at
    ])
  end
end

For completeness I also include this alternate version that uses an instance variable instead of serializable hash:

app/serializers/email/foo_serializer.rb

require 'active_model/serializer'
require 'active_support/core_ext/object'
require 'active_support/inflector'
require 'active_support/notifications'
class Email::FooSerializer < ActiveModel::Serializer
  attributes :id, :bars, :created_at, :updated_at

  def initialize(foo, precomputed_serialized_array_of_bars=nil)
    @bars = precomputed_serialized_array_of_bars
    super(foo)
  end

  def bars
    if @bars
      return @bars
    else
      serialized_bars = {}
      object.bars.each do | bar |
        serialized_bars[bar.baz] ||= []
        serialized_bars[bar.baz] << Email::BarSerializer.new(bar).attributes
      end
      return serialized_bars
    end
  end
end

Then to use serialize the top level object you would simply do

require 'app/serializers/email/foo_serializer'
require 'app/serializers/email/bar_serializer'
Email::FooSerializer.new(Foo.first).as_json

OR if you wanted to use the pre-computed serialized array of bars:

# pre-compute array_of_serialized_bars somewhere up here

require 'app/serializers/email/foo_serializer'
require 'app/serializers/email/bar_serializer'
Email::FooSerializer.new(Foo.first, array_of_pre_serialized_bars).as_json
Jesse Sanford
  • 607
  • 1
  • 8
  • 18
  • 2
    `class SerializableHash < Hash alias :read_attribute_for_serialization :[] end` This is a really bad idea for various reasons. Just. Don't. Inheriting core types in MRI has unexpected behavior. I wish I could write more. – BF4 Mar 21 '16 at 05:59