Jun 06, 2017

Rails Tip: Dynamic JSON fields

Recently I had the chance to work on a piece of software that required interacting with JSON Arrays. As you may know using native JSON columns in ActiveRecord it’s as simple as using the store_accessor. For example, let’s say we are defining a new column json_object that has to follow this JSON structure:

{
  "name": "juan perez",
  "age": 69
}

By using store_accessor in the following way:

store_accessor :json_object, :name, :age

You will define accessors to those two fields that in the end will save those said fields as a JSON to that specific column. Nothing really fancy.

But… what if we need to store and edit something like this:

[
  "value1",
  "value2",
  "value3"
]

This changes things a bit.

I built a Rails 5 application that solves this problem, feel free to follow the source code, includes specs with a 100% coverage, no smoke and mirrors.

What is the solution then?

ActiveModel to the rescue

In this Rails application example, there are two keys to solve this problem, first one is this piece of code:

# frozen_string_literal: true
class Tag
  include ActiveModel::Model

  attr_accessor :name

  validates :name, length: { maximum: 5, allow_blank: true }

  def marked_for_destruction?
    false
  end
end

The class above defines the individual element that is stored directly into the JSON column (in our example above any of "value1", "value2" or "value3").

Notice how this is a simple Ruby class that includes ActiveModel::Model, that way we can add validators and allows us it to use as a part of parent model.

Second part of the puzzle is the following concern (that is included in the model defining the table with this JSON array column).

The important bits of this concern are the following:

validates :tags, associated: true

def tags
  @tags ||= add_missing_values(build_from_column)
end

def tags_attributes=(attributes)
  write_attribute(:json_tags, attributes.values)
end
  • The validates allows us to obviously make sure the nested objects are valid and in case they are not it allows us to properly render the view correctly: error validation
  • The tags method allows us to use fields_for
  <%= form.fields_for :tags, include_id: false do |form_tags| %>
    <li>
      <%= form_tags.label :name %>
      <%= form_tags.text_field :name %>
    </li>
  <% end %>
  • And finally the tags_attributes= method acts as the setter when submitting the form.

With that you can store an array of values to a JSON column. One nice improvement to this Rails application could be to add cocoon to allow adding (and removing) any number of dynamic fields instead of the hardcoded we defined currently.

The more you know