Back

Rails 5 Attributes API

The Rails 5 attributes API is the best feature of Rails 5, but no one knows it yet. You hardly hear about it. It took me awhile to even find out how to use it.

In Rails 5, model attributes go through the attributes API when they are set from user input (or any setter) and retrieved from the database (or any getter). Rails has used an internal attributes API for it’s entire lifetime. When you set an integer field to “5”, it will be cast to 5. This API is now not only public, but can also be extended to support custom types that respond to a simple interface.

You’ve likely wished the attributes API has existed before. Anytime you’ve done something like this:

class Item < ActiveRecord::Base
  attr_accessor :price
  before_validation :convert_price_to_price_cents
   
  def price
    price_cents.fdiv 100
  end
 
  def convert_price_to_price_cents
    self.price_cents = price.to_f * 100
  end
end

 

This is the example you will see most often, and it is a valid one (though the money-rails gem takes care of this particular case for us anyway).

There are other common use-cases for custom types, just off the top of my head:

  • Converting phone number fields to e-164 format using a library like global_phone
  • Generating referral codes, or other types of tokens
  • Tracking “points” on a user, so that user.points returns Points.new(10) or whatever so that you can decorate 10 with additional points-specific methods
  • Allowing encrypted attributes to be searchable using normal ActiveRecord syntax
  • Returning null objects from missing associations

You can also use it with built in types. Let’s say you have a freeform textbox that gets converted into a date. You can define that attribute as a date and take advantage of Rails’s built-in date parsing.

Using the Attributes API

In Rails 5, ActiveRecord has an attribute class method. You should use this instead of the normal attr_accessor when defining even virtual attributes. The first argument is the field name, the second argument is the type.

class Item
  attribute :price, :integer
end

Integer here is the built in type. For custom types, give it an instance of your custom object. I’m going to use a very simple custom type for this example, called Inquiry, which returns a StringInquirer object for a string field.

class Repository
  attribute :branch, Inquiry.new
end

You should inherit from ActiveRecord::Type::Value or a more specific type, such as ActiveRecord::Type::String which I will do here, since for all other purposes, it’s a string. I will also override the type method, although at the time of this writing I have no idea what it does and there is no documentation for it.

class Inquiry < ActiveRecord::Type::String
  def type
    :inquiry
  end
end

Override cast to return what you want. The default behavior is to call cast on both setting and getting. If you only want to do setting, override deserialize instead for the getting and leave cast for setting. By default, deserialize just calls cast.

class Inquiry < ActiveRecord::Type::String
  # ...
 
  def cast(value)
    super.inquiry
  end
end

If you are wondering, #inquiry and StringInquirer are built into Rails. It’s what you get from Rails.env, it basically allows you call predicate methods that return true if the string matches the method name.

That’s it. Now when I set branch, it will cast it to a StringInquirer. When I get branch, it will cast it to a StringInquirer.

repo = Repository.new
repo.branch = 'master'
 
repo.branch.master?
#=> true

 

Default Values

You can set defaults, too:

attribute :branch, Inquiry.new, default: 'develop'

 

Querying

As I mentioned in the encryption example, this API supports where. ActiveRecord will serialize any value being queried by calling serialize on your custom type. The type passed into serialize will be the one from cast/deserialize, so in this example, a StringInquiry. Note that in this case it is not necessary to define this method since I’m inheriting from a built-in type.

class Inquiry < ActiveRecord::Type::String
  # ...
 
  def serialize(value) # value here is a StringInquiry
    value.to_s
  end
end

 

SEE OUR JOB OPENINGS

Logan Serman
Logan Serman