You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Enumerized ActiveModel::Attributes readers return a raw String on Rails 8.1 (value predicates raise NoMethodError) #482
For a plain ActiveModel object (include ActiveModel::Attributes + extend Enumerize, not ActiveRecord), enumerized attribute readers return the raw assigned value instead of an Enumerize::Value on Rails 8.1. Value-level predicate methods then raise NoMethodError.
Environment
enumerize
2.8.1 and current master (4b1c918)
Rails
8.1.3 (broken) · 8.0.5, 7.2.3.1 (unaffected)
Ruby
3.3.6
Reproduction
Self-contained, using only activemodel + enumerize:
require"active_model"require"enumerize"classFilterincludeActiveModel::ModelincludeActiveModel::AttributesextendEnumerizeenumerize:mode,in: %i[defaultfull_text],default: :defaultendf=Filter.new(mode: "full_text")f.mode.class# Rails <= 8.0: Enumerize::Value ; Rails 8.1: Stringf.mode.full_text?# Rails 8.1: NoMethodError: undefined method `full_text?' for an instance of String
Root cause
Enumerize::ActiveModelAttributesSupport::Type (a subclass of ActiveModel::Type::Value) overrides deserialize but not cast. ActiveRecord reads go through deserialize (DB value → wrapped), so ActiveRecord is unaffected. Plain ActiveModel::Attributes assignment resolves reads through cast, which returns the raw value.
It historically "worked" only because enumerize's own reader module (from define_methods!) re-wraps via find_value(super) and used to sit above the ActiveModel-generated attribute-methods module in the ancestor chain. Rails 8.1 reordered attribute-method module inclusion so the generated reader now outranks enumerize's module — exposing that cast never wrapped.
The manifestation depends on declaration order:
Declaring a plain attribute :xbeforeenumerize keeps enumerize's module on top, so the bug is hidden (this is why the existing ActiveModelUser test fixture, which declares attribute :name, :string first, still passes on Rails 8.1).
Declaring enumerizefirst (or only enumerized attributes, as above) exposes it.
Confirmed via MRO: the reader's owner is the ActiveModel-generated module, sitting above Enumerize::Module in .ancestors.
Fix
Define Type#cast to wrap values (returning invalid single values unchanged so inclusion validation still rejects them), keeping deserialize intact. This makes correctness independent of ancestor-chain ordering. I have verified it green on Rails 8.1.3 / 8.0.5 / 7.2.3.1.
This looks like the same problem that #476 already targets. I've reproduced and validated that approach. Also added model-level (predicate) regression tests + a CHANGELOG entry in persona-id#3.
Activity
Add ActiveModel::Attributes model-reader tests for Type#cast
cast values to Enumerize::Value, filter invalid values on multiple (#476
nashby commentedon Jul 18, 2026
should be fixed by #476