ruby 48 lines · 3 tabs

Normalizing Phone Numbers on Assignment with Rails normalizes and a Custom Serializer

Shared by codesnips Jul 2026
3 tabs
class User < ApplicationRecord
  normalizes :phone, with: ->(value) { PhoneNormalizer.call(value) }, apply_to_nil: false

  validates :phone,
            presence: true,
            uniqueness: { case_sensitive: false },
            format: {
              with: /\A\+[1-9]\d{6,14}\z/,
              message: "is not a valid phone number"
            }

  def self.find_by_phone(input)
    normalized = PhoneNormalizer.call(input)
    return nil if normalized.blank?

    find_by(phone: normalized)
  end
end
3 files · ruby Explain with highlit

This snippet shows how to keep phone numbers canonical at every layer: normalized to E.164 on write with ActiveRecord's normalizes, validated against that canonical form, and rendered back in a human-friendly way through a dedicated serializer. Storing one canonical representation per number avoids the classic bug where +1 (415) 555-0100 and 4155550100 are treated as different values, which breaks uniqueness checks, deduplication, and lookups.

In User model, normalizes :phone runs a normalization block whenever the attribute is assigned — not just on save — so reads after assignment already reflect the cleaned value. The block delegates to PhoneNormalizer.call, which strips formatting and coerces to E.164 using a configurable default region. Because normalization happens before validation, the format validation and the unique index both operate on the canonical string. with: -> keeps the transform pure and testable, and apply_to_nil: false leaves blank phones untouched so optional numbers stay nil rather than becoming an empty string.

PhoneNormalizer is a small PORO wrapping the phonelib gem. It returns nil for junk input rather than raising, which lets the model treat an unparseable number as a validation failure instead of an exception. Centralizing the parsing here means both the model and any importer or API endpoint share exactly one definition of "valid." The trade-off is that E.164 discards the original formatting a user typed, so display formatting must be reconstructed on the way out.

That reconstruction lives in UserSerializer, an ActiveModel::Serializer. It exposes the raw canonical phone for machine consumers and a derived phone_display in the caller's national format, again delegating to phonelib so formatting logic is not duplicated in views. Serializers are the right seam for this because presentation should not leak into the persisted value.

The main pitfall is region ambiguity: a bare 5551234 has no country, so the default region matters and should reflect the application's user base or be captured explicitly. Another is that normalizes only fires on the setter, so bulk update_all writes bypass it — those paths must normalize manually. Reach for this pattern whenever a value has a single canonical form but many valid textual inputs.


Related snips

Share this code

Here's the card — post it anywhere.

Normalizing Phone Numbers on Assignment with Rails normalizes and a Custom Serializer — share card
Link copied