Serialized Attributes in ActiveRecord 3.2

Rails has had serialized attributes for a while, and every time I try to do something fancy with them, I have to rediscover how they work.

I have used composed_of to do some of these things in the past, so take a look at it if serialize isn’t quite what you’re looking for.

The basics

To get started with serialize, add a text column to your table, and add the serialize directive to your model.

class MyModel < ActiveRecord::Base
  # This serializes whatever you assign.
  serialize :my_field
  # This serializes a hash. If the DB value is nil, the AR value will be {}
  serialize :my_field, Hash
  # This serializes some class that you've written.
  serialize :my_field, MyField
end

If you choose the last option, what aspects of serialization can you control?

Default serialization with YAML

By default, your attribute will be serialized as YAML. Let’s say you define MyField like this:

class MyField
end

It will be serialized like this:

--- !ruby/object:MyField {}

It will deserialize back to a MyField instance.

Custom serialization

You can customize the serialization in a couple of different ways. One way is to implement the instance method to_yaml. I wouldn’t do this, because you’ll need to do it in a way that Yaml.load can use to create an instance of your class.

The other way to do it is to add dump and load class methods to your class. For example:

class MyField
  def self.dump(obj)
    # ...
  end
  def self.load(string)
    # ...
  end
end

Dump converts your object to a string, and load converts a string into an instance of your class. Here’s an example of a custom formatting of attributes:

class MyField
  def self.dump(obj)
    coder.dump(:a => obj.a, :b => obj.b)
  end
  def self.load(string)
    new coder.load(string)
  end
  def self.coder
    ActiveRecord::Coders::YAMLColumn.new(Hash)
  end
  def initialize(options = {})
    @a = options[:a]
    @b = options[:b]
  end
  attr_accessor :a, :b
end

Now, let’s say you want to be able to assign a hash to the serialized object, maybe from a form. You can do that:

class MyField
  def self.dump(obj)
    case obj
    when Hash then coder.dump(obj)
    else           coder.dump(:a => obj.a, :b => obj.b)
    end
  end
  def self.load(string)
    new coder.load(string)
  end
  def self.coder
    ActiveRecord::Coders::YAMLColumn.new(Hash)
  end
  def initialize(options = {})
    @a = options[:a]
    @b = options[:b]
  end
  attr_accessor :a, :b
end

It’s worth noting that you’ll be without the instance of MyField at first.

>>> rec = MyModel.new
>>> rec.my_field.class
 => MyField
>>> rec.my_field = {}
>>> rec.my_field.class
 => Hash
>>> rec.save
 => true
>>> rec.my_field.class
 => MyField

So, another option is to override the setter on MyModel:

class MyModel < ActiveRecord::Base
  serialize :my_field, MyField
  attr_accessible :my_field
  def my_field= value
    case value
    when Hash
      super MyField.new(value)
    else
      super
    end
  end
end
>>> rec = MyModel.new
>>> rec.my_field.class
 => MyField
>>> rec.my_field = {}
>>> rec.my_field.class
 => MyField
>>> rec.save
 => true
>>> rec.my_field.class
 => MyField
>>> rec.attributes = {:my_field => {}}
>>> rec.my_field.class
 => MyField