Skip to content

Enumerized ActiveModel::Attributes readers return a raw String on Rails 8.1 (value predicates raise NoMethodError) #482

Description

@rastamhadi

Summary

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"

class Filter
  include ActiveModel::Model
  include ActiveModel::Attributes
  extend Enumerize
  enumerize :mode, in: %i[default full_text], default: :default
end

f = Filter.new(mode: "full_text")
f.mode.class      # Rails <= 8.0: Enumerize::Value ; Rails 8.1: String
f.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 :x before enumerize 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 enumerize first (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.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions