Wrapping JSON-based ActiveRecord attributes with classes
Tags: ruby, rails, json
Published: 2019-02-26 19:05:58

---
title: Wrapping JSON-based ActiveRecord attributes with classes
published: true
description: This post will teach you how to wrap JSON-based DB columns with ActiveModel-like classes _easily_
tags: ruby, rails, json
canonical_url: https://evilmartians.com/chronicles/wrapping-json-based-active-record-attributes-with-classes
---

_Posted in [Martian Chronicles](https://evilmartians.com/chronicles/wrapping-json-based-active-record-attributes-with-classes)_

Have you ever tried to denormalize your DB structure a little bit by using a [JSON-based](https://www.postgresql.org/docs/9.4/datatype-json.html) column? It goes without saying that great ~~power~~ flexibility comes with great responsibility, but in some cases, this approach does a good job and simplifies things a lot.

For instance, imagine that you are building a feature for yet another e-commerce site, which allows users to filter products by a set of rules and order them automatically each night. Obviously, we want to specify the count of products to order, but how can we represent the filtering rules (e.g. min/max price, publication date etc)? We could have a column for each rule, but JSON column looks more promising. We are not planning to filter by any of these columns, however, there is a big chance that we will add more rules in future and JSON column allows us to avoid migrating our database each time.

This is how we usually add JSONB column to the PostgreSQL table:

```ruby
create_table :auto_buyers do |t|
  t.integer :count, null: false
  t.jsonb :rules, null: false, default: {}
end
```

This attribute is just a hash:

```ruby
buyer = AutoBuyer.create(count: 1, rules: { max_price: 10 })

if item.price < buyer.rules["max_price"]
  # buy item...
end
```

What if we want something more complex, for instance, validate such fields? This looks a bit messy:

```ruby
class AutoBuyer < ApplicationRecord
  validate do
    if rules["max_price"] && rules["min_price"] && rules["min_price"] > rules["max_price"]
      errors.add(:max_price, "should be greater then min price")
    end
  end
end
```

Imagine that ActiveRecord model has its own behavior and we are adding more complexity with such validations—in this case, it might make sense to move the logic around the JSON attribute to the separate class and it would also be nice to work with attributes, not the hash. [Attributes API](https://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html) can save us!

However, the documentation is confusing enough and it’s hard to get things right from the first attempt. Let's try to do it start with defining the class for representing our hash as an object:

```ruby
class AutoBuyerRules
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :min_price, :integer
  attribute :max_price, :integer
end
```

After that, we need to define the attribute class to wrap up the hash to this class:

```ruby
class AutoBuyerRulesType < ActiveModel::Type::Value
  def type
    :jsonb
  end

  # rubocop:disable Style/RescueModifier
  def cast_value(value)
    case value
    when String
      decoded = ActiveSupport::JSON.decode(value) rescue nil
      AutoBuyerRules.new(decoded) unless decoded.nil?
    when Hash
      AutoBuyerRules.new(value)
    when AutoBuyerRules
      value
    end
  end
  # rubocop:enable Style/RescueModifier

  def serialize(value)
    case value
    when Hash, AutoBuyerRules
      ActiveSupport::JSON.encode(value)
    else
      super
    end
  end

  def changed_in_place?(raw_old_value, new_value)
    cast_value(raw_old_value) != new_value
  end
end
```

And finally, let's tell our ActiveRecord model to use this new attribute and try it out:

```ruby
class AutoBuyer < ApplicationRecord
  attribute :rules, AutoBuyerRulesType.new
end

buyer = AutoBuyer.create(count: 1, rules: { max_price: 10 })

if item.price < buyer.rules.max_price
  # buy item...
end
```

It looks way better, but should we use so much code each time we want to get this behavior? Not really, please welcome [store_model](https://github.com/DmitryTsepelev/store_model) gem!

To make the long story short (you can find the long store at the _README_) let's take a look at the same example:

```ruby
class AutoBuyerRules
  include StoreModel::Model

  attribute :min_price, :integer
  attribute :max_price, :integer

  validate :min_price, presence: true
end

class AutoBuyer < ApplicationRecord
  attribute :rules, AutoBuyerRules.to_type

  validate :rules, store_model: { merge_errors: true }
end

buyer = AutoBuyer.new
puts buyer.valid? # => false
puts buyer.errors.messages # => { min_price: ["can't be blank"] }
```

That's it, you don't need to include a ton of modules or define any custom types by hand! It also comes with validation support—you can validate store model fields and then merge these errors to the parent record (or define any custom strategy).

Let's recap:

- JSON columns are good for the data that change often
- you can handle them as hashes, but it can be verbose
- you can use Attribute API to wrap them with objects or use [store_model](https://github.com/DmitryTsepelev/store_model) gem
- if you want to do the same thing, but have attributes defined on the ActiveRecord model itself—consider using [store_attribute](https://github.com/palkan/store_attribute) or [jsonb_accessor](https://github.com/madeintandem/jsonb_accessor)

---

Read more dev articles on https://evilmartians.com/chronicles!