JT Book a call

Blog

type_for_attribute & has_attribute? in Rails 7.2 Active Model

Published

2 min read

type_for_attribute & has_attribute? in Rails 7.2 Active Model
In this post

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? #

See how we work

Read next

Tell us what you need built

JetThoughts has been building software since 2008. Book a call, or write to sales@jetthoughts.com. You'll leave the call with a written one-page scope within 2 working days.

#01 Paul Keen
Paul KeenChief Technology Officer & Open Source Contributor

A 30-minute call; pick a time.