John Tornow
Serialized Attributes

Serialized Attributes

Posted on 4 min read

One of my favorite features of ActiveRecord is the ability to provide a custom serializer to use for database-backed columns. Serialization enhances the underlying value of a column with some additional functionality, type casting, or whatever you need. The API is incredibly simple.

Here is a basic example of a JSON-serialized column:

# We're assuming the `products` table has a text column named `details`
class Product
  serialize :details, coder: JSON    
end

A basic Product model with a details text column.

In this example we have a column on the products table, used by the Product model, named details. Typically here you’d want to reach for a jsonb column type, but text columns work as well.

The implementation allows us to have some convenience methods on the model, so we can nicely work with JSON:

# setting some JSON data
product = Product.new
product.details = { "hello" => "world" }
product.details["hello"] # => "world"

# loading data from a model
product = Product.find(1)
product.details["hello"] # => "world"

That’s the basics. A simple text-backed column in your database, but it looks and feels like a hash, via the JSON class.

Serializing Strings into Objects

The basic JSON-style use case for serialized attributes is very handy and I use it often. But a lesser known, or perhaps lesser used, implementation of this feature isn’t to store custom data structures but rather to enhance a value with more information or an enum style attribute approach.

Here’s an example that I used a few years ago on an app that interacted heavily with some Amazon marketplace APIs. The APIs and use case don’t really matter here but the illustration is helpful to show how the serialized attributes can be used.

Let’s continue with our Product model, but add a marketplace string column:

class Product
  serialize :details, coder: JSON    
  serialize :marketplace, coder: Marketplace
end

The marketplace column is a simple string column. But when serialized into an object, we can add some helpful convenience functionality.

A Basic Custom Serializer

Let’s break down this Marketplace class we’re using as the coder for the serializer:

class Marketplace

  def initialize(key)
    @key = key
  end

  def to_s
    @key.to_s
  end

  def self.dump(marketplace)
    marketplace.to_s
  end

  def self.load(raw)
    new(raw)
  end

end

This is about as simple an example of a serializer as we can build. A Rails attribute serializer only needs to respond to two methods: load and dump.

Marketplace.load is how we create a new Marketplace instance from the value in the Product’s marketplace column. In this case, our string can also be nil, so make sure to account for loading nil when loading data into a serializer.

Marketplace.dump is used to turn the Marketplace instance back into a string to be saved in the database. This example is extremely simple, so we’re just turning the Marketplace back into a string using to_s.

So far we’ve created a serializer that takes a raw string and turns it into an instance of the Marketplace class, with the key as the column’s value.

Leveling Up

A basic string serializer isn’t super useful. Let’s add more value to the Marketplace class. The real benefit of using a serialized attribute on top of strings is to augment the data with other useful methods and information throughout our app.

Here’s an example. First, we’ll set a Product’s marketplace to a known value:

product = Product.new
product.marketplace = "us"

Setting up a new Product instance with “us” marketplace

Then, using some new methods and configuration data in our Marketplace class, we can have additional details about the “us” marketplace we just selected:

product.marketplace.us? 
# => true
product.marketplace.id 
# => "ATVPDKIKX0DER"
product.marketplace.currency 
# => "USD"
product.marketplace.endpoint 
# => "https://sellingpartnerapi-na.amazon.com"

Enhancing the original string serializer

Here we’re using a simple string column to match up a list of Amazon marketplaces and augmenting the string with some additional data we have in a configuration file in the app. This type of extension is super useful when writing expressive code that is clear to read. Yes, we could read the config data elsewhere, but I really like this style of syntax.

Here’s the updated Marketplace class that makes it happen:

class Marketplace

  def initialize(key)
    @marketplace = self.class.find(key).presence || {}
  end

  def aws_region
    @marketplace[:aws_region]
  end

  def currency
    @marketplace[:currency]
  end

  def id
    @marketplace[:id]
  end

  def endpoint
    @marketplace[:url]
  end

  def name
    @marketplace[:name]
  end

  def region
    @marketplace[:region]
  end

  def to_s
    @marketplace[:key]
  end

  def method_missing(method_name)
    if match = method_name.to_s.match(/^(?<key>[a-z0-9]+)\?$/)
      match[:key] == to_s
    else
      super
    end
  end

  def self.all
    @all ||= JSON.parse(File.read(Rails.root.join("config/data/marketplaces.json"))).map do |hash|
      Hash[hash.map { |k, v| [k.to_sym, v] }]
    end
  end

  def self.find(key)
    key = key.to_s
    all.detect { |hash| hash[:key] == key || hash[:id] == key }
  end

  def self.dump(marketplace)
    marketplace.to_s
  end

  def self.load(raw)
    new(raw)
  end

end

The Marketplace serializer with additional values

And, for reference, here is some JSON configuration data that I have saved in a separate file for loading in:

[
  {
    "key": "us",
    "name": "United States",
    "url": "https://sellingpartnerapi-na.amazon.com",
    "aws_region": "us-east-1",
    "id": "ATVPDKIKX0DER",
    "region": "North America",
    "currency": "USD"
  },
  {
    "key": "ca",
    "name": "Canada",
    "url": "https://sellingpartnerapi-na.amazon.com",
    "aws_region": "us-east-1",
    "id": "A2EUQ1WTGCTBG2",
    "region": "North America",
    "currency": "CAD"
  }
]

I use this sort of serialized attribute all over my apps. At its simplest level, this is just a string column in a database. But with a little extra manipulation, we now have a very expressive system for enhancing our boring column attributes.

Happy serializing!

👋
Thanks for reading. Liked this post or found it helpful? Want to tell me why everything is wrong and your way is better? I’m here for it.
Drop me a line any time.
Posted by John Tornow in: Utilities