How I Used ActiveRecord#serialize with a Custom Data Type

Zachary Porter, Former Senior Developer

Article Category: #Code

Posted on

In case you're unfamiliar, ActiveRecord#serialize is a method that allows an attribute to be saved in the database and later retrieved as an object. The common example given involves storing a user's preferences like so:

# Serialize a preferences attribute.
class User < ActiveRecord::Base
  serialize :preferences
end

user = User.new
user.preferences = { send_spam: false, send_deals: true }
user.save # => preferences saved as YAML in a text field, later retrieved as a Hash

You can even enforce the type of the object stored in the column:

class User < ActiveRecord::Base
  serialize :preferences, Hash
end

user = User.new
user.preferences = 'lolwut'
user.save # => raises ActiveRecord::SerializationTypeMismatch

Pretty neat stuff, but it requires the database column to be a text field. What if I want the database column to be a different type? Well, I recently dug into the Rails source code to find out how serialize works and what's required to use it with a custom data type.

I had a feature that allowed a user to manage a time duration in minutes and seconds. This time duration would be stored as the total amount of seconds in the database so that it could easily be converted to other units of time. It would be ideal if that seconds field had an integer type. After several iterations, I settled on a Duration class that would handle the conversions. The next step was to figure out how to store and retrieve an instance of Duration in the database.

Upon inspection of the serialize method within the Rails source, I discovered this interesting conditional (copied here for convenience):

coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) }
  class_name
else
  Coders::YAMLColumn.new(class_name)
end

So, if the provided class_name responds to load and dump, then the serialize method will use that class. Otherwise, it will fallback to the Coders::YAMLColumn class to handle the loading and saving to the database. I took a look at how the load and dump methods were implemented on the Coders::YAMLColumn class:

def dump(obj)
  return if obj.nil?

  unless obj.is_a?(object_class)
    raise SerializationTypeMismatch,
      "Attribute was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}"
  end
  YAML.dump obj
end

def load(yaml)
  return object_class.new if object_class != Object && yaml.nil?
  return yaml unless yaml.is_a?(String) && yaml =~ /^---/
  obj = YAML.load(yaml)

  unless obj.is_a?(object_class) || obj.nil?
    raise SerializationTypeMismatch,
      "Attribute was supposed to be a #{object_class}, but was a #{obj.class}"
  end
  obj ||= object_class.new if object_class != Object

  obj
end

To briefly summarize the above code, the dump method takes an object and returns the value to be stored in the database, and the load method takes the database value and returns an instance of the specified class.

I took everything that I learned about the serialize method and applied it in a custom Duration data type:

class Duration
  # Used for `serialize` method in ActiveRecord
  class << self
    def load(duration)
      self.new(duration || 0)
    end

    def dump(obj)
      unless obj.is_a?(self)
        raise ::ActiveRecord::SerializationTypeMismatch,
          "Attribute was supposed to be a #{self}, but was a #{obj.class}. -- #{obj.inspect}"
      end

      obj.length
    end
  end


  attr_accessor :minutes, :seconds

  def initialize(duration)
    @minutes = duration / 60
    @seconds = duration % 60
  end

  def length
    (minutes.to_i * 60) + seconds.to_i
  end
end

Then, in my ActiveRecord model, I added the following snippet of code:

serialize :duration_field, Duration

delegate :minutes, :minutes=, :seconds, :seconds=, to: :duration_field

And there we have it -- a lightweight class that's able to take advantage of a method provided by the Rails framework.

What do you think? Are you using the serialize method with your custom data types? Let me know in the comments below.

Related Articles