Use the Attributes API to make your Rails models more communicative

The Ruby on Rails logo.

ActiveRecord is the powerful object-relational mapper at the heart of Ruby on Rails. By default, it gives you tools to quickly and easily create new database tables and map them to domain models. It follows the ethos of “convention over configuration” that David Heinemeier Hansson coined with the release of Rails. As such, with little application code, you get a powerful, database-backed model that Just Works™.

However, few lines of code do not, necessarily, mean that the lines that are there are easy to understand. Does this model have a name or a description? How is that expiry field encoded, as a Date or a Time? What fields exist on this table, again? These are examples of common questions when working in a Ruby on Rails application.

To alleviate some of these issues, you can use the Attributes API when defining your models to make them more explicit. You can also use richer data types for your fields to ease working with subsets of your models. This article will share the history of the Attributes API, then show some concrete examples of using it.

What is the Attributes API?

Starting in Rails 4.2, then-maintainer of ActiveRecord, Sage Griffin, added an API to separate the concerns of the columns of the underlying database from the attributes that ActiveRecord sets on its record models. This helped to make the internals of ActiveRecord cleaner and easier to modify. As with many decoupling scenarios, this had the added benefit of allowing independent changes to the two systems and, thus, opening a public API for use by Rails application developers.

Before this change, the ActiveRecord directly set and modified the attributes of its models based on columns mapped from the database. The original design coupled these two things in the interest of making it easy to work with Rails; it was a good solution for the time and served its purpose well. However, it was also a limiting design in that, if you needed to color outside the lines of the framework, it was difficult to do.

As an illustration, consider this problem. You have a ticket model that contains a state machine controlling the transitions between the reserved, issued, redeemed, and voided states. You reserve a ticket while the customer purchases it. After the purchase, you issue the ticket. Once the customer arrives at the event, you redeem it. And if for any reason, you cancel the ticket, you consider it voided.

Figure 1: The state machine for the ticket example.

The important parts of this example are the following:

  1. There are event names for transitioning between different states (e.g. purchase).
  2. Some states cannot transition to other states (e.g. reserved cannot transition to redeemed).

This state machine is an example of logic that you should encapsulate in a “plain old Ruby object” and use as a collaborator in your model. Without the Attributes API, you might do the following:

class Ticket < ApplicationRecord
  # field: state
  def state
    TicketState.new(super)
  end
end

This isn’t harmful in itself, but it’s a symptom of missing functionality in the framework. A trickier bit to work around, and the source of why API exists, involves domain objects and SQL queries. For example:

state = TicketState.new("issued")
Ticket.where(state: state)

This SQL query requires you to handle the serialization of the TicketState class to the database. Your TicketState class shouldn’t know anything about databases; it’s only a state machine! Alternatively, you might attempt to always remember to write this query as Ticket.where(state: state.state). If you’re anything like me, you will forget this at the worst possible time: in the middle of an emergency.

The Attributes API gives you a place to encode all of this information and make it easy to use rich domain models as fields in your ActiveRecord models.

Using the Attributes API

There are two pieces to the Attributes API that you need to know about to use it:

  1. ActiveRecord::Type classes for defining the types of data in your models.
  2. attribute declarations in your model files.

There’s a lot of functionality within those two concepts, so let’s dig in.

ActiveRecord types

Within the Rails code base, type classes live in three different modules. ActiveRecord::Type classes inherit from ActiveModel::Type that implement similar behavior for use in ActiveModel and, sometimes, are only aliases to constant in ActiveModel. PostgreSQL-specific type classes exist in the ActiveRecord::ConnectionAdapters::PostgreSQL::OID module.

The contract for these classes consists of a slim, four methods1:

  1. #cast(value) performs a type cast on input, like when using the setter methods from accepting a form submission.
  2. #serialize(value) casts the value from the Ruby type to the type needed by the database.
  3. #deserialize(value) casts the value from the database into the Ruby type.
  4. #type is a Symbol corresponding to the Symbol used to register it with the type registry.

Unless you’re defining your own types — and if you are, stay tuned for my next article — you won’t ever use these methods directly. However, it’s helpful to know the interface in case you see some unexpected behavior and need to diagnose the issue.

Attribute declarations

Within your models, you can use the attribute class method to declare an attribute with a type and any additional information about it. For example, these are all valid examples:

class MyModel < ApplicationRecord
  # A simple, documenting example
  attribute :hosted_on, :date

  # Have a default? Set it here! (Overrides database defaults.)
  attribute :likes, default: 0

  # Is your default something that needs recalculated?
  attribute :expiry, default: -> { 10.minutes.from_now }

  # For the JSONB type in PostgreSQL, like a document model
  attribute :metadata, :jsonb

  # Have a PostgreSQL array field? Mark it like so:
  attribute :tags, :string, array: true

  # How about a PostgreSQL date range?
  attribute :valid_in, :date, range: true
end

Attributes don’t need a database column associated with them. If you have some datum that you need to accept but not persist, you can create an attribute for it, set validations for it, and even use dirty tracking if you want!

Sadly, there is no API for listing the registered types. In Rails 6.1, you can run the following, but it’s not guaranteed to work in the future:

ActiveRecord::Type
  .registry
  .instance_variable_get(:@registrations)
  .map { |r| r.__send__(:name) }

The built-in types for all databases mirror the types you can use in your migrations, though with some different names (e.g. bigint in migrations vs. big_integer in attributes). Gems and database adapters may also add extra options so if you’re doing something exotic, it’s worth a look under the hood to see what’s available.

Benefits of declaring your attributes

There are three benefits to declaring your attributes in your models. First, you make the data model more discoverable when you onboard a new person into your project. Second, you use the uniform access principle to define different types of attributes for your model. And lastly, you enable richer behavior for your fields when it’s appropriate.

Convention over configuration means there’s little ceremony to working in Rails, which allows you to move quickly. But it comes at the cost of discoverability for future you and your teammates. When you end up with a large model, it’s easy to forget the name of a value or what type it is. Adding an attribute declaration, particularly when you have a standard ordering to the paragraphs of code in your models, makes it easy to reference in the file when you’re working with it.

Have you ever had to define a virtual attribute on a model? For example, you might have a confirmation system where you have your customers check a box, but don’t persist that to the database. If so, you probably defined that attribute using standard methods. That’s reasonable, but the Attributes API allows you to define that attribute just like a database-backed one so that all of the model’s attributes exist in the same paragraph of code. That makes it easier to see the shape of the class and what it does. This is an application of the uniform access principle, in a way, because it groups like things under the same interface.

Like we saw with the ticket example above, we sometimes need to have complex collaborators as part of our models. In the ticket’s case, we wanted a state machine to manage the state of the class but had to resort to using a wrapping method. Instead, what if we could define our own type to handle that behavior:

class Ticket < ApplicationRecord
  attribute :state, :ticket_state
end

This is a teaser of my next article, so check in next week to see how to do this!

Everything isn’t sunshine and rainbows though; there are detriments to using the Attributes API as well.

Detriments of the API

Like with anything in life, there are trade-offs to using the Attributes API. The two big ones are the introduction of a new tool and boilerplate.

There’s a common saying about projects where you only have so much “innovation budget” that you can spend on a project and hope that it succeeds. This guiding principle helps you to control the impulse to always use the hot, new technology or process on every project. Making decisions based on blog posts is one of the things you have to consider in this budget! There is overhead to using this technique. It’s relatively new to Rails, so you will likely have to teach it to your colleagues. Because it’s new, there will be friction for people using it for the first time. And because it’s something that you had to teach, you should also document the decision as well so that future you and your colleagues know why you made that decision.

The second detriment comes down to requiring boilerplate, which is not the Rails Way. You already have the information in the schema, so why duplicate it in the model when Rails can discover it for you? You also can make standard methods for the class, so why do it this way? Do you always use the Attributes API or only sometimes? The decision to use something that can seem like boilerplate is fraught. I get that. But I believe that the trade-off is worth it.

Conclusion

The Attributes API allows you to customize the way that Rails casts data into fields, both database-backed and virtual. The API is a public part of Rails so has certain compatibility guarantees around it. It’s powerful and extensible. And it solves many problems using a small amount of code, both at the application level and the framework level.

There are trade-offs to using the Attributes API. I believe the costs are worth it, particularly for complex problem sets. But if you have a limited amount of teaching ability in your team, it might be something to pause and think about before you adopt it.

This article was just a primer on how to use it. Make sure to read my next article, where I will show you how to customize it!

Have you used the Attributes API in a project? What was your experience?


  1. I think the #type method is necessary. It’s marked with a “no doc” comment so that indicates that it’s not, but I have never seen a type without it. If I’m wrong, let me know!