Using store_accessor with JSONB Columns
When you have a PostgreSQL jsonb column, you can access its keys like regular model attributes using store_accessor. You get attribute setters, dirty tracking, and validations without any extra configuration.
How It Works
Given a users table with a data jsonb column:
{ "username": "emma", "age": 25 }
Declare the accessors in the model:
class User < ApplicationRecord
store_accessor :data, :username, :age
end
Now username and age behave like regular attributes:
user = User.last
user.username # => "emma"
user.age # => 25
user.username = "bob"
user.username_changed? # => true
user.username_was # => "emma"
To see all declared accessors for a store:
User.stored_attributes[:data]
# => [:username, :age]
Under the hood, store_accessor calls store from ActiveRecord::Store with the column name and generates getter/setter pairs that read from and write to the hash stored in that column. Because PostgreSQL handles the serialization natively for jsonb, no coder: option is needed.
Validations Work Out of the Box
Since the accessors are treated as model attributes, standard validations apply:
class User < ApplicationRecord
store_accessor :data, :username, :email
validates :username, presence: true
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
end
Nesting Limitation
store_accessor only works with top-level keys. Say your companies table has a details jsonb column with this structure:
{
"address": { "city": "Taipei", "country": "Taiwan" },
"contact": { "email": "[email protected]", "phone": "+886-2-1234-5678" },
"business_hours": { "open": "09:00", "close": "18:00" }
}
Declaring store_accessor :details, :address gives you the whole address hash, not city or country as individual attributes:
company.address # => {"city"=>"Taipei", "country"=>"Taiwan"}
company.city # NoMethodError
To access nested keys as attributes, combine store_accessor with attribute:
class Company < ApplicationRecord
store_accessor :details, :address, :contact, :business_hours
attribute :address, :jsonb
store_accessor :address, :city, :country
attribute :contact, :jsonb
store_accessor :contact, :email, :phone
attribute :business_hours, :jsonb
store_accessor :business_hours, :open, :close
end
The attribute :address, :jsonb tells ActiveRecord to treat address as a typed attribute, so the nested store_accessor has something to bind to.
company.city # => "Taipei"
company.email # => "[email protected]"
company.open # => "09:00"
company.city = "Kaohsiung"
company.city_changed? # => true
If you find yourself doing this for many nested keys, it's worth reconsidering whether those fields should be dedicated columns instead.
JSONB + store_accessor vs Dedicated Columns
Use store_accessor with JSONB when:
- The attributes are optional or sparse (not every record has them)
- The set of keys is likely to change over time (feature flags, per-tenant settings, user preferences)
- The fields are supplementary and you don't need to query them frequently or index them
Use dedicated columns when:
- You need to filter, sort, or join on the field regularly
- The field is required for most records
- You want database-level constraints like
NOT NULLor foreign keys