This blog is part of our Rails 6 series. Rails 6.0 was recently released.

Before Rails 6

Before Rails 6, the default format %{attribute} %{message} is used to display validation error message for a model’s attribute.

>> article = Article.new
=> #<Article id: nil, title: nil, description: nil, created_at: nil, updated_at: nil>
>> article.errors.full_message(:title, "cannot be blank")
=> "Title cannot be blank"

The default format can be overridden globally using a language-specific locale file.

# config/locales/en.yml

en:
  errors:
    format:
      "'%{attribute}' %{message}"

With this change, the full error message is changed for all the attributes of all models.

>> article = Article.new
=> #<Article id: nil, title: nil, description: nil, created_at: nil, updated_at: nil>
>> article.errors.full_message(:title, "cannot be blank")
=> "'Title' cannot be blank"

>> user = User.new
=> #<User id: nil, first_name: nil, last_name: nil, country: nil, created_at: nil, updated_at: nil>
>> user.errors.full_message(:first_name, "cannot be blank")
=> "'First name' cannot be blank"

This trick works in some cases but it doesn’t work if we have to customize the error messages on the basis of specific models or attributes.

Before Rails 6, there is no easy way to generate error messages like shown below.

The article's title cannot be empty

or

First name of a person cannot be blank

If we change the errors.format to The article's %{attribute} %{message} in config/locales/en.yml then that format will be unexpectedaly used for other models, too.

# config/locales/en.yml

en:
  errors:
    format:
      "The article's %{attribute} %{message}"

This is what will happen if we make such a change.

>> article = Article.new
=> #<Article id: nil, title: nil, description: nil, created_at: nil, updated_at: nil>
>> article.errors.full_message(:title, "cannot be empty")
=> "The article's Title cannot be empty"

>> user = User.new
=> #<User id: nil, first_name: nil, last_name: nil, country: nil, created_at: nil, updated_at: nil>
>> user.errors.full_message(:first_name, "cannot be blank")
=> "The article's First name cannot be blank"

Notice the error message generated for the :first_name attribute of User model. This does not look the way we want, right?

Let’s see what is changed in Rails 6 to overcome this problem.

Enhancements made to ActiveModel::Errors#full_message in Rails 6

Overriding the format of error message globally using errors.format is still supported in Rails 6.

In addition to that, Rails 6 now also supports overriding the error message’s format at the model level and at the attribute level.

In order to enable this support, we need to explicitly set config.active_model.i18n_customize_full_message to true in the Rails configuration file, preferably in config/application.rb which is implicitly set to false by default.

Overriding model level format

We can customize the full error message format for each model separately.

# config/locales/en.yml

en:
  activerecord:
    errors:
      models:
        article:
          format: "`%{attribute}`: %{message}"
        user:
          format: "%{attribute} of the user %{message}"

The full error messages will look like this.

>> article = Article.new
=> #<Article id: nil, title: nil, description: nil, created_at: nil, updated_at: nil>
>> article.errors.full_message(:title, "cannot be empty")
=> "`Title`: cannot be empty"
>> article.valid?
=> false
>> article.errors.full_messages
=> ["`Title`: can't be blank"]

>> user = User.new
=> #<User id: nil, first_name: nil, last_name: nil, country: nil, created_at: nil, updated_at: nil>
>> user.errors.full_message(:first_name, "cannot be blank")
=> "First name of the user cannot be blank"

>> comment = Comment.new
=> #<Comment id: nil, message: nil, author_id: nil, created_at: nil, updated_at: nil>
>> comment.errors.full_message(:message, "is required")
=> "Message is required"

Notice how the default format %{attribute} %{message} is used for generating the full error messages for the Comment model since its format is not being overridden.

Since the other methods such as ActiveModel::Errors#full_messages, ActiveModel::Errors#full_messages_for, ActiveModel::Errors#to_hash etc. use the ActiveModel::Errors#full_message method under the hood, we get the full error messages according to the custom format in the returned values of these methods respectively as expected.

Overriding attribute level format

Similar to customizing format at the model level, we can customize the error format for specific attributes of individual models.

# config/locales/en.yml

en:
  activerecord:
    errors:
      models:
        article:
          attributes:
            title:
              format: "The article's title %{message}"
        user:
          attributes:
            first_name:
              format: "%{attribute} of a person %{message}"

With such a configuration, we get the customized error message for the title attribute of the Article model.

>> article = Article.new
=> #<Article id: nil, title: nil, description: nil, created_at: nil, updated_at: nil>
>> article.errors.full_message(:title, "cannot be empty")
=> "The article's title cannot be empty"
>> article.errors.full_message(:description, "cannot be empty")
=> "Description cannot be empty"

>> user = User.new
=> #<User id: nil, first_name: nil, last_name: nil, country: nil, created_at: nil, updated_at: nil>
>> user.errors.full_message(:first_name, "cannot be blank")
=> "First name of a person cannot be blank"
>> user.errors.full_message(:last_name, "cannot be blank")
=> "Last name cannot be blank"

Note that the error messages for the rest of the attributes were generated using the default %{attribute} %{message} format for which we didn’t add custom formats in the config/locales/en.yml manifest.

Overriding model level format of deeply nested models
# config/locales/en.yml

en:
  activerecord:
    errors:
      models:
        article/comments/attachments:
          format: "%{message}"
>> article = Article.new
=> #<Article id: nil, title: nil, description: nil, created_at: nil, updated_at: nil>
>> article.errors.full_message(:'comments/attachments.file_name', "is required")
=> "is required"
>> article.errors.full_message(:'comments/attachments.path', "cannot be blank")
=> "cannot be blank"
>> article.errors.full_message(:'comments.message', "cannot be blank")
=> "Comments message cannot be blank"
Overriding attribute level format of deeply nested models
# config/locales/en.yml

en:
  activerecord:
    errors:
      models:
        article/comments/attachments:
          attributes:
            file_name:
              format: "File name of an attachment %{message}"
>> article = Article.new
=> #<Article id: nil, title: nil, description: nil, created_at: nil, updated_at: nil>
>> article.errors.full_message(:'comments/attachments.file_name', "is required")
=> "File name of an attachment is required"
>> article.errors.full_message(:'comments/attachments.path', "cannot be blank")
=> "Comments/attachments path cannot be blank"

Precedence

The custom formats specified in the locale file has the following precedence in the high to low order.

  • activerecord.errors.models.article/comments/attachments.attributes.file_name.format
  • activerecord.errors.models.article/comments/attachments.format
  • activerecord.errors.models.article.attributes.title.format
  • activerecord.errors.models.article.format
  • errors.format

To learn more, please checkout rails/rails#32956 and rails/rails#35789.