type_for_attribute & has_attribute? in Rails 7.2 Active Model

type_for_attribute & has_attribute? in Rails 7.2 Active Model

Method signature #

ClassName.type_for_attribute(attribute_name) # => ActiveModel::Type::Value subclass

Rails 7.2 moved type_for_attribute from Active Record to Active Model. Any class that includes ActiveModel::Attributes now exposes the same type lookup that Active Record models have always had. The method returns the type object Rails uses internally to cast values, so you can ask “what is the declared type of :age?” without instantiating the model.

The change shipped in Rails PR #51653 and is documented in the Rails 7.2 release notes .

Image description

Active Model example #

class MyModel
  include ActiveModel::Attributes

  attribute :my_attribute, :integer
end

MyModel.type_for_attribute(:my_attribute)
# => #<ActiveModel::Type::Integer ...>

MyModel.type_for_attribute(:my_attribute).cast("42")
# => 42

The return value is an ActiveModel::Type::Value subclass - Integer, String, Boolean, DateTime, and so on - with cast, serialize, and deserialize methods.

Class method vs instance access #

type_for_attribute is a class method. You call it on the class, not an instance:

MyModel.type_for_attribute(:my_attribute)   # ✓ works
MyModel.new.type_for_attribute(:my_attribute) # NoMethodError

To inspect a value on a specific instance, combine it with the attribute getter:

record = MyModel.new(my_attribute: "42")
type   = MyModel.type_for_attribute(:my_attribute)
type.cast(record.my_attribute) # => 42

has_attribute? is the sibling method that returns true/false for whether an attribute exists, without raising on unknown keys. It’s an instance method (the inverse of type_for_attribute’s class-method shape):

record = MyModel.new(my_attribute: 42)

record.has_attribute?(:my_attribute) # => true
record.has_attribute?(:unknown)      # => false

Use both together when you want to introspect dynamic input: has_attribute? checks whether the key is defined, then type_for_attribute(key).cast(value) casts it. That’s the pattern in the SignupForm example below.

Active Record still works the same way #

class User < ApplicationRecord
end

User.type_for_attribute(:email)
# => #<ActiveModel::Type::String ...>

Active Record inherits the method through the same code path now, so the API is identical across both layers.

Use case: validating form input by declared type #

class SignupForm
  include ActiveModel::Attributes

  attribute :email, :string
  attribute :age, :integer

  def initialize(params)
    params.each do |key, value|
      raise ArgumentError, "Unknown attribute: #{key}" unless has_attribute?(key.to_sym)

      type = self.class.type_for_attribute(key.to_sym)
      public_send("#{key}=", type.cast(value))
    end
  end
end

form = SignupForm.new(email: "test@example.com", age: "42")
form.age # => 42 (cast from "42")

Before Rails 7.2 you had to maintain a parallel hash of attribute types or duplicate the Active Record helper. The method does one job: hand back the type object so you can cast or inspect it.

Why it matters #

If you build form objects, command objects, or any plain Ruby class that uses ActiveModel::Attributes, you get the same introspection Active Record models have. No more attributes.fetch(:age).type workarounds. No custom registries.

Building Rails apps that ship? #

We rescue Rails projects that other shops left in a broken state - upgrades, performance work, test coverage. If your team is stuck on an older Rails version or your app is bleeding money on N+1 queries, talk to us.

Book a 30-minute Rails review

Comments