Rails 7.1 adds ActiveRecord::Base::normalizes

Abhijith Sheheer

By Abhijith Sheheer

on May 9, 2023

This blog is part of our  Rails 7 series.

Rails 7.1 has introduced a new method in Active Record that can be used to declare normalizations for attribute values. This can be especially useful for sanitizing user input, ensuring consistent formatting, or cleaning up data from external sources.

Before Rails 7.1, you could normalize attributes using before_save callback.

1model User < ApplicationRecord
2  before_save :downcase_email, if :email_present?
3
4  private
5
6    def email_present?
7      email.present?
8    end
9
10    def downcase_email
11      email.downcase!
12    end
13end

In Rails 7.1, you can refactor the above to the below code.

1model User < ApplicationRecord
2  normalizes :email, with: -> email { email.downcase }
3end

The normalization is applied when the attribute is assigned or updated, and the normalized value will be persisted to the database. The normalization is also applied to the corresponding keyword argument of finder methods. This allows a record to be created and later queried using unnormalized values.

By default, the normalization will not be applied to nil values. To normalize nil value, you can enable it using :apply_to_nil option.

1model User < ApplicationRecord
2  normalizes :user_name, with:
3    -> user_name { user_name.parameterize.underscore }
4
5  normalizes :email, with: -> { _1.strip.downcase }
6
7  normalizes :profile_image, with:
8    -> profile_image {
9      profile_image.present? ? URI.parse(profile_image).to_s :
10      "https://source.boringavatars.com/beam" },
11    apply_to_nil: true
12end
1# rails console
2>> User.create!(user_name: "Eve Smith", email: "eveSmith@EXAMPLE.com")
3
4#<User:0x000000010b757090 id: 1, user_name: "eve_smith", profile_image:"https://source.boringavatars.com/beam", email: "evesmith@example.com", created_at: Wed, 03 May 2023 07:49:20.067765000 UTC +00:00, updated_at: Wed, 03 May 2023 07:49:20.067765000 UTC +00:00>
5
6>> user = User.find_by!(email: "EVESMITH@example.COM")
7>> user.email # => "evesmith@example.com"
8
9>> User.exists?(email: "EveSmith@Example.COM")          # => true

If a user's email was already stored in the database before the normalization statement was added to the model, the email will not be retrieved in the normalized format.

This is because in the database it's stored in mixed case, that is, without the normalization. If you have legacy data, you can normalize it explicitly using the Normalization#normalize_attribute method.

1# rails console
2>> legacy_user = User.find(1)
3>> legacy_user.email  # => "adamSmith@EXAMPLE.com"
4>> legacy_user.normalize_attribute(:email)
5>> legacy_user.email  # => "adamsmith@example.com"
6>> legacy_user.save

Please check out this pull request for more details.

Stay up to date with our blogs. Sign up for our newsletter.

We write about Ruby on Rails, ReactJS, React Native, remote work,open source, engineering & design.