Rails Forms, JSON Columns, and Nested Parameters

Shaker Islam
4 min readNov 27, 2017

Looks like you’ve decided to use a JSON column for your PostgreSQL (or in MySQL 5.7.8!) database. There are lots of reasons why developers end up using JSON columns — whether it’s just to store a blob of data, or you’re prototyping something and want to move quickly, JSON columns allow for a lot of flexibility when developing applications.

One of the nicer conveniences of JSON data is the ability to have nested data. In the past, if you wanted nested data in relational databases, you had to do INNER JOINs across a join table. This became an even bigger headache if your data is nested multiple levels. But with JSON columns, that complexity is washed away. You might be using them for that reason, and using Rails forms to update them in a sane way.

Here I’ll go into building Rails forms that update JSON columns with nested data.

Updating with Nested Parameters

Let’s say you have the model Article and you have to update the JSON content. Your form probably looks like this:

<%= form_with(model: article, local: true) do |form| %>
<%= form.label :content %>
<%= form.text_area :content %>
<% end %>

Perhaps, for certain articles, you want to render some special text if it’s in the content, so you want to split out the body text separately. And maybe, there’s a list of items you optionally want to show. For those cases, you can build your form like so:

<%= form_with(model: article, local: true) do |form| %>
<%= label_tag 'article[content][body]', 'Body' %>
<%= text_area_tag 'article[content][body]', article.content['body'] %>
<%= label_tag 'article[content][text]', 'Text' %>
<%= text_field_tag 'article[content][text]', article.content['text'] %>

<% article.content['list_items'].each do |item| %>
<%= label_tag 'article[content][][list_item][text]', 'Item' %>
<%= text_field_tag 'article[content][][list_item][text]', item['text'] %>
<% end %>
<% end %>
# And this is how it'll look in your controller:{
"article": {
"content": {
"body": "Body text here",
"text": "Very special text",
"list_items": [
{"text": "Item 1 text"},
{"text": "Item 2 text"}
]
}
}
}

There’s a few things to point out:

  1. We’re not using the Rails fields_for helper. Unfortunately, the fields_for helper assumes that an attribute can be called as a method on your object, as in article.content.text. This can be solved by representing your content as a Ruby object or a Hashie::Mash, for example
  2. As a result, we’re using the lower-level label_tag and text_field_tag instead of form.label so we can represent the correct data and ids

Permitted Parameters

Remember that you have to whitelist parameters in Rails. That includes nested parameters! Here’s what it should look like:

def post_params
params.require(:article)
.permit(content: [
:body,
:text,
list_items: [:text]])
end

Checkboxes

Adding checkboxes to a form is a good way to represent boolean flags that you want to update. Normally, the Rails check_box helper will add a hidden input tag before your checkbox, like so:

<input name="article[flag]" type="hidden" value="0">
<input type="checkbox" value="1" name="article[flag]">

However, this gets a little tricky with nested arrays. As noted in the Form Helper docs, Rails depends on duplicate parameters in order to give the checkbox precedence over the hidden tag, if it’s checked. This logic is actually found in Rack when it parses form data for a Rack::Request that Rails and ActionDispatch eventually pass through to your controller. It basically looks like this:

article[flag]=0 # this is data from the hidden input tag
article[flag]=1 # this is from the actual checkbox, always _after_ the hidden tag
params = {} # for simplicity, let's assume params is a Hashparams[article][flag] = 0
params[article][flag] = 1 # the checkbox "overwrites" the data from the hidden tag

This only works if your checkbox is in a nested Hash — but if it’s an Array, Rack doesn’t have a way of figuring out which Array element has to be overwritten with subsequent checkbox data.

The good news is that you may not need a hidden tag to begin with. If you’re updating a JSON column, the absence of a value is sufficient to determine a “false” state. Thus, you can use a check_box_tag. Let's add it to our example before:

<% article.content['list_items'].each do |item| %>
<%= label_tag 'article[content][][list_item][text]', 'Item' %>
<%= check_box_tag 'article[content][][list_item][flag]', true, ActiveRecord::Type::Boolean.new.cast(item['flag']) %>
<%= text_field_tag 'article[content][][list_item][text]', item['text'] %>
<% end %>

Remember that the form is going to send your data as a String, and if you have a nested boolean in your JSON, that’ll be persisted as a String since Rails won’t iterate through the JSON to convert it. The method ActiveRecord::Type::Boolean.new.cast allows you to cast that String to and from a boolean as you need.

Summary

We looked at how to update a JSON column through Rails forms, particularly nested data, and covered a few gotchas with checkboxes. This should get you started with building out your forms. I also highly recommend reading through the Form Helper docs to familiarize yourself with what’s possible.

If you have any questions or have a better way, my handle is @shay_ker on Twitter. And if you found this helpful,

--

--

Shaker Islam

Sometimes serious. Mostly harmless. I write about tech and personal stuff. Being silly elsewhere on the interwebs @__smiz or on Clubhouse.