Reading the Source · Card 11
What does attribute add?
A declaration most Rails developers haven't used. It's how you add typed attributes — virtual ones, or overrides for existing columns. Quietly powerful.
The familiar (or maybe unfamiliar) line
Two uses. The first has no matching database column. The second overrides the type of one.
class User < ApplicationRecord
# 1. Virtual attribute. Not in the schema. Lives only on the instance.
attribute :discount_percent, :integer, default: 0
# 2. Override the type of an existing column.
# Suppose the column `metadata` is text in the DB; treat it as JSON in Ruby.
attribute :metadata, :json, default: {}
end The question
What does each of those two attribute calls actually buy you, and when would you reach for them?
Take a moment. Most Rails attributes come from columns automatically. This declaration adds typed attributes on top of that mechanism. Why would you do that?
The two use cases
Virtual attributes. Add an attribute that lives on the instance without needing a column. You get a reader, writer, default, type casting, and dirty tracking — same as a real column. Useful for form-only fields, computed temporary state, or service-object inputs.
user = User.new
user.discount_percent # => 0 (the default)
user.discount_percent = "15" # cast to Integer
user.discount_percent # => 15
user.discount_percent_changed? # => true (dirty tracking works) Type overrides. Take an existing column (e.g. a text column storing JSON) and read it as the right Ruby type. Rails does the casting on the way in and out.
user.metadata # => {} (Hash, not a JSON string)
user.metadata = { theme: "dark", lang: "fr" }
user.save!
# In the database, metadata is stored as JSON text.
# In Ruby, you got a Hash, and you assigned a Hash.
# You can also register custom types (e.g. for a Money value object):
class MoneyType < ActiveRecord::Type::Value
def cast(value); Money.from_cents(value.to_i); end
def serialize(money); money.cents; end
end
ActiveRecord::Type.register(:money, MoneyType)
class Order < ApplicationRecord
attribute :total, :money
end
order.total = Money.from_cents(2500)
order.total.cents # => 2500 Where this happens in Rails
Defined in activerecord/lib/active_record/attributes.rb. The method registers a type for the named attribute. Type classes live under activerecord/lib/active_record/type/ and activemodel/lib/active_model/type/. Each type defines cast (Ruby in), serialize (Ruby out to the database), and deserialize (database in to Ruby).
Why this is worth knowing
Most Rails developers reach for attr_accessor for virtual attributes, which works but loses dirty tracking, defaults, and casting. attribute gives you all three for free. And when you need a value object (Money, Coordinates, Color) backed by a primitive column, registering a custom type is the senior-flavored move that keeps the rest of the app reading clean Ruby instead of strings or hashes.