The Rails 6.1 will probably be released this year, and with it comes the major changes in ActiveModel Errors. I want to explain the rationale behind the change, and how we can prepare for the upgrade.

Wrap each error as an object

When our model object contains invalid data, the valid? call would fill up the errors information. Historically this behaved like a hash which maps attribute to error messages. However at Rails 5.0, a separate details hash was added for accessing additional information.

The two hashes are supposed to be a one-to-one mapping, but finding one message’s corresponding details is actually a chore. You need to get the index of the message, then use that to access the second hash.

The two hashes can also get out of sync in a few edge cases. Keeping internal state consistent is indeed difficult, and for this reason, in 6.1, we applied object oriented design to bundle the relevant error information together.

Now under the hood of book.errors is an array of Error objects.

As with all big changes, there will be deprecations and breaking changes which requires updates. I will list the most important things below:

How to upgrade?

Message and details

Instead of accessing messages, full_messages and details, which covers all errors, each individual Error object knows about its own information:

e = ActiveModel::Error.new(model, :title, :too_long, count: 1000)
e.message # 'is too long (maximum is 1000 characters)'
e.full_message # 'Title is too long (maximum is 1000 characters)'
e.detail # { count: 1000, error: :too_long }

Enumeration methods

Previously, errors behaves like a hash and we do this:

book.errors.each do |attribute, error_message|
  puts attribute
  puts error_message
end

For compatibility, this would result in deprecation message Rails 6.1. What you can do is to use the single arity enumerator:

book.errors.each do |error|
  puts error.attribute
  puts error.message
end

As shown above, if the block takes only one parameter, it would return the Error objects directly. So when you call first, those would also return the Error object.

Avoid modifying the hashes directly

In the past you can add new errors by appending messages directly on to the hash:

book.errors[:title] << 'is not interesting enough.'
book.errors[:title].clear

This would now also raise a deprecation warning, because it is no longer the source of truth, but generated from the Error objects when called. Instead always use the add interface, or enumeration methods:

book.errors.add(:title, 'is not interesting enough.')
book.errors.delete_all { |error| error.type == :too_long }

Manipulating the following will have no effect:

  • errors itself as a hash (e.g. errors[:foo] = 'bar')
  • the hash returned by errors#messages (e.g. errors.messages[:foo] = 'bar')
  • the hash returned by errors#details (e.g. errors.details[:foo].clear)

Removal of hash-like interface

As we move towards the array like data representation, several hash-like interfaces will be deprecated and removed. These include:

  • errors#slice!
  • errors#values
  • errors#keys

Misc

  • errors#to_xml will be removed.
  • errors#to_h will be removed, and can be replaced with errors#to_hash.

New things

To help filtering the errors, a new where method is provided. Its method signature is different to ActiveRecord query method though. You can filter by:

  • attribute name (required)
  • error type (optional)
  • options (optional)

Only supplied params will be matched.

book.errors.where(:name) # => all errors related to name attribute.
book.errors.where(:name, :too_short) # => all name attribute errors of being too short
book.errors.where(:name, :too_short, minimum: 2) # => all name attribute errors of being too short and minimum being 2

The new import method (which merge! uses) allows errors to be nested. This is especially helpful for association validation or ActiveInteraction, where deep nesting can occur. Nested error opens up possibility to access more information in such cases.

delete method now accepts more granular filters, so you can delete specific type of error within an attribute.

Conclusion

If you are interseted, you can check out the original pull request. You can also read the official doc for more information.

As the code is still changing constantly, and me forgetting things here and there, there are probably some errors in this post. I’ll keep this updated, but apology in advance.

If you have some suggestion please feel free to leave a comment below or open an issue on Rails repository. Thanks!