Rails 5 Active Record attributes API
Rails 5 made the Active Record Attributes API public, allowing you to define custom attribute types and convert values to appropriate Ruby types. This API provides fine-grained control over how attributes are handled in your models.
What is the Attributes API?
The Attributes API allows you to:
- Define custom attribute types
- Convert values to appropriate Ruby types
- Override default type casting behavior
- Add type-specific behavior to attributes
Basic Usage
Define Custom Attribute Type
Create a custom attribute type:
# app/models/concerns/encrypted_string_type.rb
class EncryptedStringType < ActiveModel::Type::String
def cast(value)
return nil if value.nil?
Encryptor.decrypt(super(value))
end
def serialize(value)
return nil if value.nil?
Encryptor.encrypt(value)
end
end
# Use in model
class User < ApplicationRecord
attribute :ssn, EncryptedStringType.new
end
Using Built-in Types
Use built-in Active Model types:
class Post < ApplicationRecord
attribute :published_at, :datetime
attribute :views_count, :integer
attribute :price, :decimal
attribute :is_featured, :boolean
end
Real-World Examples
Money Type
Create a money attribute type:
# app/models/concerns/money_type.rb
class MoneyType < ActiveModel::Type::Decimal
def cast(value)
return nil if value.nil?
case value
when String
BigDecimal(value.delete('$,'))
when Numeric
BigDecimal(value.to_s)
else
super
end
end
def serialize(value)
return nil if value.nil?
value.to_f
end
end
# Use in model
class Product < ApplicationRecord
attribute :price, MoneyType.new
end
# Usage
product = Product.new(price: "$1,234.56")
product.price # => #<BigDecimal:...>
JSON Type
Handle JSON attributes:
class Post < ApplicationRecord
attribute :metadata, :json
end
# Usage
post = Post.new(metadata: { tags: ['ruby', 'rails'], views: 100 })
post.metadata # => { "tags" => ["ruby", "rails"], "views" => 100 }
Array Type
Handle array attributes:
class User < ApplicationRecord
attribute :tags, :string, array: true
end
# Usage
user = User.new(tags: ['developer', 'ruby'])
user.tags # => ["developer", "ruby"]
Type Casting
Custom Casting Logic
Override casting behavior:
class PercentageType < ActiveModel::Type::Decimal
def cast(value)
return nil if value.nil?
case value
when String
BigDecimal(value.delete('%')) / 100
when Numeric
BigDecimal(value) / 100
else
super
end
end
end
class Discount < ApplicationRecord
attribute :percentage, PercentageType.new
end
# Usage
discount = Discount.new(percentage: "25%")
discount.percentage # => 0.25
Attribute Defaults
Setting Defaults
Set default values using the Attributes API:
class Post < ApplicationRecord
attribute :status, :string, default: 'draft'
attribute :views_count, :integer, default: 0
attribute :published_at, :datetime, default: -> { Time.current }
end
Advanced Usage
Custom Type with Validation
Combine with validations:
class EmailType < ActiveModel::Type::String
def cast(value)
return nil if value.nil?
value = super(value)
raise ArgumentError, "Invalid email" unless value.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
value
end
end
class User < ApplicationRecord
attribute :email, EmailType.new
end
Type Aliases
Create type aliases:
# config/initializers/attribute_types.rb
ActiveRecord::Type.register(:money, MoneyType)
ActiveRecord::Type.register(:percentage, PercentageType)
# Use in models
class Product < ApplicationRecord
attribute :price, :money
end
class Discount < ApplicationRecord
attribute :percentage, :percentage
end
Benefits
The Attributes API provides:
- Type safety: Ensure attributes are the correct type
- Custom behavior: Add type-specific logic
- Reusability: Share types across models
- Consistency: Standardized type handling
Use Cases
Encrypted Attributes
class EncryptedStringType < ActiveModel::Type::String
def cast(value)
return nil if value.nil?
Encryptor.decrypt(super(value))
end
def serialize(value)
return nil if value.nil?
Encryptor.encrypt(value)
end
end
class User < ApplicationRecord
attribute :ssn, EncryptedStringType.new
end
Formatted Numbers
class PhoneNumberType < ActiveModel::Type::String
def cast(value)
return nil if value.nil?
value = super(value).gsub(/\D/, '')
"(#{value[0..2]}) #{value[3..5]}-#{value[6..9]}"
end
end
class Contact < ApplicationRecord
attribute :phone, PhoneNumberType.new
end
Best Practices
- Keep types focused: Each type should handle one concern
- Handle nil values: Always check for nil before processing
- Use built-in types: Prefer built-in types when possible
- Document custom types: Add comments explaining type behavior
Conclusion
Rails 5's public Attributes API provides powerful tools for defining custom attribute types and controlling how values are cast and serialized. This API enables you to create type-safe, reusable attribute handling that can be shared across your application, making your models more robust and maintainable.