

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?
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
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: 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 %>
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.