ActiveTypedStore
active_typed_store
is a lightweight (105 lines of code) and highly performant gem (see benchmarks)
designed to help you store and manage typed data in JSON format within database.
This gem provides a simple, yet powerful way to ensure that your JSON data cast
to specific types, enabling more structured and reliable use of JSON fields in your Rails models.
You can use ActiveRecord Types
for simplicity or Dry Types
for more advanced features such as
constraints and type composition. You can also combine both approaches
in the same model to get the best of both worlds.
Installation
Add this line to your application's Gemfile:
gem "active_typed_store"
Usage
Using ActiveRecord Types
class Model < ActiveRecord::Base
typed_store(:params) do # params - the name of the store
attr :task_id, :integer
attr :name, :string
attr :notify_at, :datetime
attr :asap, :boolean, default: false
attr :settings, :json
end
end
m = Model.first
m.task_id = "123" # string
m.task_id # => 123, int
m.task_id? # => true, value.present? under the hood
m.asap? # => false
m.asap = "yes"
m.asap # => true
m.asap? # => true
attr(name, type, options)
-
name
the name of the accessor to the store -
type
a symbol such as:string
or:integer
, or a type object to be used for the accessor -
options
(optional), a hash of cast type options such as:-
precision
,limit
,scale
-
default
the default value to use when no value is provided. Otherwise, the default will be nil -
array
specifies that the type should be an array
-
Using Dry Types
class Model < ActiveRecord::Base
typed_store(:params) do
attr :task_id, Types::Params::Integer
attr :name, Types::Params::String
attr :notify_at, Types::Params::Time
attr :asap, Types::Params::Bool.default(false)
attr :email, Types::String.constrained(format: /@/)
attr :settings, Types::Params::Hash
end
end
Combine ActiveRecord and Dry Types
class Model < ActiveRecord::Base
typed_store(:params) do
attr :price, :decimal, scale: 2
attr :active, :immutable_string
attr :email, Types::String.constrained(format: /@/)
attr :state, Types::String.enum('draft', 'published', 'archived')
attr :tariff_id, Types::Array.of(Types::Params::Integer)
end
end
Hash safety
This gem assumes you're using a database that supports structured data types, such as json
in PostgreSQL
or MySQL
, and leverages Rails' store_accessor under the hood. However, there’s one caveat: JSON columns use a string-keyed hash and don’t support access via symbols. To avoid unexpected errors when accessing the hash, we raise an error if a symbol is used. You can disable this behavior by setting config.hash_safety = false
.
class Model < ActiveRecord::Base
typed_store(:params) do
attr :price, :decimal
end
end
record = Model.new(price: 1)
record["price"] # 1
record[:price] # raises "Symbol keys are not allowed `:price` (ActiveTypedStore::SymbolKeysDisallowed)"
# initializers/active_type_store.rb
ActiveTypeStore.configure do |config|
config.hash_safety = false # default :disallow_symbol_keys
end
record["price"] # 1
record[:price] # nil - isn't the expected behavior for most applications
Benchmarks
compare active_typed_store
with other gems
# ruby 3.3.5 arm64-darwin24
# gem getter i/s setter i/s Lines of code
# active_typed_store: 28502.2 656 105
# rails (without types): 27350.5 660 170
# store_attribute: 24592.2 - 1.16x slower 639 276
# store_model: 22833.6 - 1.25x slower 595 857
# attr_json: 14000.4 - 2.03x slower 577 - 1.14x slower 1195
# jsonb_accessor: 13995.4 - 2.04x slower 626 324
License
The gem is available as open source under the terms of the MIT License.