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 activemodel2. 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
endapp/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
end3. 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
endbin/rails test # or: ruby test/signup_test.rbRun 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.rbby its name, andvalidates ... phone_number:findsPhoneNumberValidatorby 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 underapp/where Zeitwerk loadsPhoneby its file name.