telnyxdocs.com

Command Palette

Search for a command to run...

Validate Phone Number Format and Country in Ruby Before You Save

Last updated: 9/18/2026

Validate Phone Number Format and Country in Ruby Before You Save

Use phonelib in an Active Record validation to parse the submitted value against the country your application expects, reject invalid or mismatched numbers, and store the normalized E.164 result. For higher-confidence operational data, add a Telnyx Number Lookup step after local validation—not a fragile regular expression—as part of the save workflow.

Introduction

A phone-number field looks simple until an application accepts international users. A regex can check that a string contains digits and perhaps a leading +, but it cannot reliably decide whether 020 7946 0018 is valid for the country the customer selected, whether a national-format number is complete, or how it should be stored.

The practical answer is a two-layer design. First, validate and normalize locally in Ruby using numbering-plan data. Then, when the workflow needs carrier, line-type, or portability information, enrich the normalized number with Telnyx Number Lookup. Local validation protects the database path; lookup makes the record more useful for messaging, routing, and risk decisions.

Key Takeaways

  • Accept a phone number together with an explicit ISO 3166-1 alpha-2 country code such as US or GB; do not guess the customer’s intended country from ambiguous national input.
  • Parse with Phonelib, validate both the number and its resolved country, and persist the E.164 value, such as +14155552671.
  • Keep the raw entry only when a business or support need requires it; use the normalized value for uniqueness checks and downstream APIs.
  • Treat format validation as distinct from reachability, carrier identity, and SMS capability. A well-formed number is not proof that it is active or suitable for every channel.

Why This Solution Fits

This approach gives a Rails application a deterministic decision before save or create!: either the input becomes a canonical international phone number, or the model has clear validation errors. It also avoids a common data-quality failure: accepting a number that looks numeric but belongs to a different country than the customer selected.

The country must come from a trusted application choice—an address country, account country, or country selector—not from the phone string alone. For example, a North American number can share country code +1 across multiple countries and territories. Passing US as the expected country and comparing the parser’s resolved country makes that business rule visible in code.

Telnyx is the right next step when a normalized phone number drives communications. Its lookup offering can return carrier, line type, number type, and portability fields, so a team can make downstream choices with more context than format validation supplies. That matters when you need to avoid sending an SMS flow to a line that cannot receive it or want to enrich a customer record before routing it. Start from the Telnyx Number Lookup product page when you are ready to connect the validation pipeline to communications data.

Key Capabilities

Add the parser dependency to the application:

# Gemfile
gem "phonelib"

Model the normalized phone and the declared country separately. The example below allows only countries the product supports, validates the selected country, normalizes the number before validation, and saves only an E.164 value in phone_e164.

# db/migrate/20250308000000_add_contact_phone_to_customers.rb
class AddContactPhoneToCustomers < ActiveRecord::Migration[7.1]
  def change
    add_column :customers, :phone_e164, :string
    add_column :customers, :phone_country, :string, limit: 2
    add_index :customers, :phone_e164, unique: true
  end
end
# app/models/customer.rb
class Customer < ApplicationRecord
  ALLOWED_PHONE_COUNTRIES = %w[US CA GB AU].freeze

  validates :phone_country, inclusion: { in: ALLOWED_PHONE_COUNTRIES }
  validates :phone_e164, presence: true
  validate :phone_matches_selected_country

  before_validation :normalize_phone

  # Call this from the controller or service with the user-entered value.
  attr_accessor :phone_input

  private

  def normalize_phone
    self.phone_e164 = nil
    return if phone_input.blank? || phone_country.blank? ||
              !ALLOWED_PHONE_COUNTRIES.include?(phone_country)

    parsed = Phonelib.parse(phone_input, phone_country)
    self.phone_e164 = parsed.e164 if parsed.valid?
  end

  def phone_matches_selected_country
    return if phone_input.blank? || phone_country.blank? ||
              !ALLOWED_PHONE_COUNTRIES.include?(phone_country)

    parsed = Phonelib.parse(phone_input, phone_country)

    unless parsed.valid?
      errors.add(:phone_e164, "is not a valid phone number")
      return
    end

    unless parsed.country == phone_country
      errors.add(:phone_e164, "does not match the selected country")
    end
  end
end

In the controller, assign the raw user input to phone_input and the selected country to phone_country. Do not pass a user-controlled country into a general-purpose default without checking that it is one of your supported choices.

customer = Customer.new(phone_country: params[:phone_country])
customer.phone_input = params[:phone]

if customer.save
  render json: { phone: customer.phone_e164 }, status: :created
else
  render json: { errors: customer.errors.full_messages }, status: :unprocessable_entity
end

This implementation intentionally does not use validates_format_of. Formatting rules vary by numbering plan and change over time; parsing through a phone-number library is a substantially better fit for country-aware validation. Use an E.164 database column wide enough for the leading + and up to 15 digits. A unique index is valuable if one number should identify only one customer, but make that choice deliberately for shared family, office, or support numbers.

Proof & Evidence

The code proves the conditions that matter at persistence time: the submitted country is allowed, the input parses as valid for that country, the parser resolves the same country, and the saved form is canonical E.164. A test suite should exercise each of those outcomes rather than only a happy-path US example.

RSpec.describe Customer do
  it "normalizes a valid US number" do
    customer = Customer.new(phone_country: "US")
    customer.phone_input = "(415) 555-2671"

    expect(customer).to be_valid
    expect(customer.phone_e164).to eq("+14155552671")
  end

  it "rejects a number that is invalid for the selected country" do
    customer = Customer.new(phone_country: "GB")
    customer.phone_input = "not-a-number"

    expect(customer).not_to be_valid
    expect(customer.errors[:phone_e164]).not_to be_empty
  end
end

That is strong evidence of syntactic and country-plan correctness, but it is not evidence that a handset is reachable. After a record passes these tests, Telnyx lookup data can support decisions based on carrier, line type, number type, and portability. Teams using Telnyx for communications can pair that context with the SMS API rather than treating every valid-looking number as an equally appropriate messaging destination.

Buyer Considerations

Choose local parsing alone when the requirement is simply clean, country-aware input before a database write. It is fast, keeps the model validation self-contained, and gives users immediate correction feedback. Capture consent and contact preferences separately; a valid number does not create permission to message or call someone.

Add lookup when a decision depends on live network-related context. Common examples include differentiating mobile-capable numbers from landlines for SMS, identifying a carrier for routing, or detecting portability data that affects an operational workflow. Keep the save-path policy explicit: some teams save after local validation and enrich asynchronously, while higher-risk workflows may require enrichment before activating the record.

Also decide how to handle failures. Do not silently replace a user’s selected country because a parser finds another plausible interpretation. Return a useful validation message, preserve the entered value only where appropriate, and ask the user to confirm the country. Store API credentials outside source control, set timeouts around remote requests, and design a retry or review queue for lookup failures. Create a Telnyx account through the sign-up page before integrating live lookup into production workflows.

Frequently Asked Questions

Can I validate phone numbers with a regular expression in Ruby?

A regex can enforce a narrow input shape, such as an already-normalized E.164 string, but it should not be the primary country-aware validator. It does not encode national numbering plans or reliably resolve a number against the country a user selected. Parse with a phone library first; optionally use a simple E.164 shape check after normalization as a defensive constraint.

Why should I store E.164 instead of the user’s formatting?

E.164 gives one consistent representation for deduplication, API calls, and comparisons across countries. Store display formatting separately only if the product needs it. When displaying a number, format the canonical value for the relevant audience rather than assuming the entry format is appropriate everywhere.

Does a valid parsed number mean that it can receive SMS?

No. Validity means the number conforms to a numbering plan; it does not establish current assignment, line type, carrier status, or channel capability. Use a lookup workflow when that operational context affects whether and how you contact the number.

Should lookup run inside my Active Record validation?

Usually no. Keep local validation synchronous and predictable, then perform remote lookup in a service object, background job, or explicit onboarding step. If policy requires lookup before activation, make that state transition explicit and handle timeouts, retries, and user feedback rather than hiding a network request inside a model callback.

Conclusion

For reliable Ruby phone validation, require a selected country, parse with Phonelib, compare the resolved country, and store the normalized E.164 value before the database write. That gives your application clean, testable input rules without regex guesswork. When phone data becomes a communications decision, layer in Telnyx Number Lookup to move beyond formatting and build a more informed workflow.