The Evolution of ActiveModel::Error in the Rails Framework

The Evolution of ActiveModel::Error in the Rails Framework

Rails progression emphasizes simplicity and productivity. Through versions, Rails integrated tools, enhanced performance, and adapted to industry standards, keeping a focus on developer happiness and efficiency. ActiveModel::Error is an example of that. On this blog post, we’ll dive into the evolution of this object.

If you find yourself trying to upgrade your Rails application, it’s very likely that you will have to deal with changes that were made to ActiveModel::Error over time.

There are some key differences between versions 5.0 and 6.0 of Rails on how it deals with validation errors in the model layer.

Back in Rails 3.0 ActiveModel::Error was inheriting from the ActiveSupport::OrderedHash class, and it responded to Hash methods. It already included methods like full_messages, details, getters and setters.

# Rails 3.0 - activemodel/lib/active_model/errors.rb
class Errors < ActiveSupport::OrderedHash

This ActiveSupport::OrderedHash inheritance was changed to an initializer later on Rails 3.1 to instantiate the error’s messages attribute.

# Rails 3.1 - activemodel/lib/active_model/errors.rb
def initialize(base)
  @base     = base
  @messages = ActiveSupport::OrderedHash.new
end

Rails 4.0 changed the messages attribute initialization from ActiveSupport::OrderedHash to use the Hash class, changing the behavior of the to_xml, as_json, and to_hash methods. This change was made to adjust to Ruby version 1.9 or higher as Rails 4 had a minimum requirement of Ruby 1.9++ by that time. You can check the pull request here opens a new window .

# Rails 4.0 - activemodel/lib/active_model/errors.rb
def initialize(base)
  @base     = base
  @messages = {}
end

Lately, in Rails 5.0 they included the details attributes in the initializer to determine what validator has failed. You can check the pull request here opens a new window .

def initialize(base)
  @base     = base
  @messages = {}
  @details  = Hash.new { |details, attribute| details[attribute] = [] }
end

Once this commit opens a new window got in the Rails repository, they started deprecating getters, setters and []= methods because of inconsistent behavior when dealing with then:

errors.messages[:key] and errors.get(:key) can be accessed only by symbol, but errors["key"] can be access by both string or symbol

errors.set(:key, ["error"]) is overwriting all errors, but errors[:key] = "error" (which should do the same thing) pushes error to existing ones.

errors.set("key", ["error"]) will not convert key to symbol, but errors["key"] = "error" and errors.add("key", :invalid) will convert it.

You can check the discussion in this pull request opens a new window .

Rails 5.0 also deprecated the add_on_empty and add_on_blank methods in a following commit opens a new window .

Rails 6.0 brings new methods to ActiveModel::Error:

slice, that was already provided to Hash via ActiveSupport

of_kind? that allow us to check presence of a specific error

and adds a new configuration option to customize format of the full_message output

Rails 6.1 changes ActiveModel::Error to improve its object orientation behavior in this pull request opens a new window . The changes include a query interface, enable more precise testing, and access to error details.

model.errors.where(:name, :foo, bar: 3).first

Also making it easier to find the message with corresponding details of one particular error:

# model.errors.details
{:name=>[{error: :foo_error, count: 1}, {error: :bar_error}, {error: :foo_error, count: 3}]}

This change also opened the possibility of advancing modding, as errors are now objects so we can add functionality on top of them. For example, we can create custom methods to disable global attribute prefixes on an error’s full messages.

Some methods like as_json, add, and include? remain unchanged after this Rails 6.1 change. Other methods were deprecated (full_message, generate_message, has_key) and new methods were added to this class: messages_for, where, and import.

The change tries its best at maintaining backward compatibility, however some edge cases won’t be covered. For example, errors#first will return ActiveModel::Error and manipulating errors.messages and errors.details hashes directly will have no effect.

Rails 7.0 removes all previous deprecated methods from ActiveModel::Error from the code.

Conclusion

In conclusion, the ActiveModel::Error emerges as an essential component within the Rails environment, playing a crucial role in the robustness and adaptability of modern Rails applications. Its ability to seamlessly handle and manage validation errors not only enhances the user experience by providing clear and concise error messages but also contributes significantly to the overall maintainability of the code. By encapsulating error details within this specialized object, developers can streamline error handling processes, ensuring a more efficient and organized approach to troubleshooting and debugging.

ActiveModel::Error stands as a testament to Rails’ commitment to simplicity, convention over configuration, and the creation of applications that are not only powerful but also resilient and developer-friendly.

Running an end-of-life Rails version in production and in need of an upgrade? Send us a message, we can help! opens a new window

Get the book