Skip to content

Validate phone numbers in Ruby on Rails

Phone number validation in Ruby on Rails that knows the national numbering plans: what people type is read in the plans you pick, a number no plan accepts gets Ruby on Rails's usual error, and what you store is its E.164 form. The reading is done by one file with no dependency, from the Ruby guide; the rest is Ruby on Rails's own validation hook, a few lines long.

1. The file

Take phone.rb from the Ruby guide, with the plans of the countries you serve (the link opens it for the United Kingdom, the United States and France, the plans the tests below use). It needs Ruby 3.0 or later; no dependency. Its Phone.parse reads a number as typed and returns the number with its E.164 form, region and line type, or nil when no plan accepts it.

# Rails includes Active Model; outside Rails: gem install activemodel

2. The Ruby on Rails side

app/validators/phone_number_validator.rb

The validator: the language guide's parse in an EachValidator.

# Validates that an attribute is a phone number readable in the plans of phone.rb:
#   validates :phone, phone_number: { default_region: "GB" }
class PhoneNumberValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    return if Phone.parse(value.to_s, options[:default_region])

    record.errors.add(attribute, options[:message] || "is not a valid phone number")
  end
end

app/models/signup.rb

A model that uses it, and the form to store.

class Signup
  include ActiveModel::Model

  attr_accessor :name, :phone

  validates :name, presence: true
  validates :phone, presence: true, phone_number: { default_region: "GB" }

  # What to store: the E.164 form, once the number is valid.
  def phone_e164
    Phone.parse(phone.to_s, "GB")&.e164
  end
end

3. The tests

test/signup_test.rb: The tests, with minitest.

require "minitest/autorun"
require "active_model"
require_relative "../phone"
require_relative "../app/validators/phone_number_validator"
require_relative "../app/models/signup"

class SignupTest < Minitest::Test
  def test_national_number_in_the_default_region
    signup = Signup.new(name: "Ada", phone: "07400 123456")
    assert signup.valid?
    assert_equal "+447400123456", signup.phone_e164
  end

  def test_international_number
    assert Signup.new(name: "Ada", phone: "+1 201-555-0123").valid?
  end

  def test_error_on_the_field
    signup = Signup.new(name: "Ada", phone: "+44 7400 12345")
    refute signup.valid?
    assert_equal ["is not a valid phone number"], signup.errors[:phone]
  end
end
bin/rails test   # or: ruby test/signup_test.rb

Run before publishing with activemodel 8.1.4 and the file generated for GB, US, FR, as 2026-10-25.

What is specific to Ruby on Rails

  • Rails loads app/validators/phone_number_validator.rb by its name, and validates ... phone_number: finds PhoneNumberValidator by convention.
  • In an Active Record model (Rails 7.1 and later), normalizes :phone, with: ->(p) { Phone.parse(p, "GB")&.e164 || p } stores the E.164 form and leaves an invalid input for the validator to report.
  • Keep phone.rb under lib/ and require it from an initializer, or under app/ where Zeitwerk loads Phone by its file name.