store_attribute icon indicating copy to clipboard operation
store_attribute copied to clipboard

Typed keys in `store_attribute` not casted correctly unless `default:` is specified

Open glebson1988 opened this issue 7 months ago • 0 comments

Summary

When using store_attribute to define typed accessors (e.g. :decimal, :datetime, etc.), the type casting works only if the default: option is passed. Without it, the value remains uncasted (e.g. stays a string after creation or DB load).

This leads to unexpected behavior where typed store attributes silently lose their type unless a default is provided.


🔁 Reproduction Steps

Given a model:

# billing_method.rb
class BillingMethod < ApplicationRecord
  store_attribute :settings, :amount, :decimal, precision: 15, scale: 2
end

Try this in Rails console:

bm = BillingMethod.create(settings: { amount: "12345.67" })
bm.reload

bm.amount
# => "12345.67" (String) ❌ expected BigDecimal

Now, change the definition:

store_attribute :settings, :amount, :decimal, precision: 15, scale: 2, default: 0

Then:

bm = BillingMethod.create(settings: { amount: "12345.67" })
bm.reload

bm.amount
# => #<BigDecimal:...,'0.1234567E5'> ✅ correct!

💡 Analysis Looking at the store_attribute implementation, _define_store_attribute is only called if:

_define_store_attribute(store_name) if
  !_local_typed_stored_attributes? ||
  _local_typed_stored_attributes[store_name][:types].empty? ||
  (options.key?(:default) && _local_typed_stored_attributes[store_name][:owner] != self)

This means that if the store is already initialized and no default: is present, _define_store_attribute is skipped. As a result, the TypedStore is not properly set up, and no type casting occurs.

I just tried to delete this part with options.key?(:default):

_define_store_attribute(store_name) if
  !_local_typed_stored_attributes? ||
  _local_typed_stored_attributes[store_name][:types].empty? ||
  _local_typed_stored_attributes[store_name][:owner] != self

Removing the condition on options.key?(:default) so that _define_store_attribute is always run when needed.

Right now I use workaround. Explicitly provide a default: when declaring the typed store attribute:

store_attribute :settings, :amount, :decimal, precision: 15, scale: 2, default: 0

BTW here is the PR where the changes were made.

glebson1988 avatar May 25 '25 10:05 glebson1988