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

  1. Keep types focused: Each type should handle one concern
  2. Handle nil values: Always check for nil before processing
  3. Use built-in types: Prefer built-in types when possible
  4. 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.

Rails 5 Active Record attributes API - Abhay Nikam