Skip to main content

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 NULL or foreign keys

References