Skip to content

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

Closed

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

Activity

  1. nashby commented on Jul 18, 2026

    @nashby
    Member

    should be fixed by #476

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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