← Blog

How to validate a phone number in Java with libphonenumber

libphonenumber runs in your JVM for free and handles most of the job — here is how to use it, and the one thing it quietly cannot tell you.

Contents

You need to validate a phone number in Java, and you have probably already found the right answer: Google's libphonenumber. It is the reference implementation — the same library that ships inside Android's dialer — it is written in Java, and it runs entirely inside your JVM. No network call, no API key, no per-lookup cost. For most of what people mean by "validate a phone number," it is the whole job. Start there.

This post shows the four calls that cover format checking, E.164 normalization, and line type, then draws a precise line: where libphonenumber stops, why its bundled carrier data is a trap, and where a service like Boundstone honestly picks up.

Validate a phone number in Java with libphonenumber

Add the dependency (check Maven Central for the latest version):

<dependency>
  <groupId>com.googlecode.libphonenumber</groupId>
  <artifactId>libphonenumber</artifactId>
  <version>8.13.50</version>
</dependency>

Then parse, validate, format, and classify:

import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat;
import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberType;
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
import com.google.i18n.phonenumbers.NumberParseException;

public class PhoneCheck {
    public static void main(String[] args) {
        PhoneNumberUtil util = PhoneNumberUtil.getInstance();
        try {
            // Second arg is the default region for numbers without a "+" prefix.
            PhoneNumber number = util.parse("+16504472983", "US");

            boolean valid = util.isValidNumber(number);
            String e164 = util.format(number, PhoneNumberFormat.E164);
            PhoneNumberType type = util.getNumberType(number);

            System.out.println("valid: " + valid);  // true
            System.out.println("e164:  " + e164);    // +16504472983
            System.out.println("type:  " + type);    // FIXED_LINE_OR_MOBILE
        } catch (NumberParseException e) {
            System.out.println("unparseable: " + e.getMessage());
        }
    }
}

Four calls do the work. PhoneNumberUtil.getInstance() hands you the singleton — it is thread-safe, so hold one instance. parse(number, region) turns a string into a structured PhoneNumber; the region tells it how to read a number typed without a country code. isValidNumber(proto) is the one you want — it checks the number against the length and prefix rules for its region, not just that it looks phone-shaped. format(proto, PhoneNumberFormat.E164) gives you the canonical +16504472983 form you should store; the E.164 explainer covers why that format is the one to keep.

What isValidNumber actually checks — and what it doesn't

isValidNumber answers one question honestly: is this a possible, correctly-structured number for its region? That rules out the transposed digit, the number that is one digit short, the area code that was never allocated. It is real validation and it catches a large share of bad input at zero cost.

What it does not do — and does not claim to — is tell you whether the number is in service right now, or who carries it. getNumberType returns MOBILE, FIXED_LINE, VOIP, or often FIXED_LINE_OR_MOBILE when the numbering plan does not separate the two (US mobile and landline numbers are drawn from the same pools, so North American numbers frequently land here). That is a metadata classification, not a live status. If you want the concept spelled out, see what phone line type means.

The carrier trap: PhoneNumberToCarrierMapper is offline metadata

libphonenumber ships a class that looks like it solves carrier detection:

import com.google.i18n.phonenumbers.PhoneNumberToCarrierMapper;
import java.util.Locale;

PhoneNumberToCarrierMapper carrier = PhoneNumberToCarrierMapper.getInstance();
String name = carrier.getNameForNumber(number, Locale.ENGLISH);
// e.g. "Verizon" — but this is the carrier the block was ALLOCATED to.

Read the comment twice. getNameForNumber returns the carrier that a number block was originally allocated to, from static tables bundled inside the JAR. It does not place a query. The moment a subscriber ports their number to another network — which people do constantly — that bundled answer is wrong, and it stays wrong until you upgrade the library and the maintainers refresh the data. It is an educated guess with a stale timestamp.

This is exactly why Boundstone's /v1/verify/phone reports carrier_lookup as not_performed rather than shipping you libphonenumber's offline guess dressed up as a live result. A wrong carrier name is worse than an honest "we didn't check" — one you will act on, the other you will route around. The three fields it names — carrier_lookup, ported_status, and hlr_liveness — each need a live query against the carrier network or a portability database and cost money per lookup, so they run as a paid opt-in: send hlr:true for a real dip at 5 credits, refunded when the network cannot answer. Left off, the field says so.

When one library isn't enough: a contract across services

If your whole stack is Java, libphonenumber may genuinely be all you need — say it plainly, ship it, move on. The API earns its place when either of two things is true.

The first is polyglot reality. Your Java service validates numbers, your Node worker validates numbers, your Python batch job validates numbers — three libphonenumber versions with three slightly different bundled datasets, drifting apart on a slow schedule. Boundstone gives every language one HTTP endpoint returning one shape, so "valid" means the same thing in every service. The second is the layer past format: the carrier and liveness data libphonenumber structurally cannot provide, delivered as one call instead of a second vendor integration.

The response makes the boundary explicit:

{
  "valid": true,
  "e164": "+16504472983",
  "country": "US",
  "line_type": "fixed_line_or_mobile",
  "national_format": "(650) 447-2983",
  "checks": {
    "performed": ["format", "region", "line_type_metadata"],
    "not_performed": ["carrier_lookup", "ported_status", "hlr_liveness"]
  }
}

Every response carries both arrays. performed is ["format", "region", "line_type_metadata"] — the same ground libphonenumber covers, because it is libphonenumber underneath. not_performed is ["carrier_lookup", "ported_status", "hlr_liveness"] — the things nobody checked, named so you never mistake a structural "valid" for a live one. The not-performed list is the point: it is what lets you trust the "valid" you did get.

Calling the Boundstone API from Java

Same number, over HTTP, using the JDK's built-in java.net.http:

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.boundstone.io/v1/verify/phone"))
    .header("Authorization", "Bearer bs_live_YOUR_KEY")
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString("{\"phone\":\"+16504472983\"}"))
    .build();

HttpResponse<String> response =
    client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());

The free tier is 250 credits a month, no card, and credits never expire. You can also paste a number into the keyless phone validator to see the exact contract before you write a line of code.

The short version

  • To validate a phone number in Java, use libphonenumber: parse, isValidNumber, format(..., E164), getNumberType. It is free, offline, and often enough — start and possibly stop there.
  • PhoneNumberToCarrierMapper is bundled static data, not a live carrier lookup. Porting makes it wrong. Do not treat it as real-time truth.
  • Reach for the API when you want one identical contract across languages, or the carrier/HLR layer libphonenumber cannot provide. Boundstone marks carrier_lookup, ported_status, and hlr_liveness as not_performed today — named, not hidden — so a "valid" means exactly what it says.

Frequently asked questions

What's the difference between isValidNumber() and isPossibleNumber() in libphonenumber?

libphonenumber gives you two checks. isPossibleNumber() is a fast length-and-prefix test that tells you whether a number could plausibly exist for a region, while isValidNumber() runs the full pattern match against that region's numbering metadata to confirm the number fits a real assigned range. Use isPossibleNumber() for quick input filtering and isValidNumber() when you want the stricter format guarantee. Neither call contacts a carrier, so a valid result confirms the number is well-formed, not that it is currently in service.

Can libphonenumber tell if a phone number is real, active, or reachable?

No. libphonenumber validates a number against static numbering-plan metadata, so it can confirm the format is correct and that the number falls within an assigned range for its region, but it cannot tell you whether the line is currently active, reachable, or who the subscriber is. Confirming a number is live requires a carrier or HLR lookup, which is a separate network query that libphonenumber does not perform. Boundstone's phone endpoint follows the same honest boundary: every response lists carrier_lookup, ported_status, and hlr_liveness as not_performed, so a valid result is never dressed up as reachable.

How do I get the line type (mobile vs landline) of a phone number in Java with libphonenumber?

Parse the number and call PhoneNumberUtil.getNumberType(), which returns values such as MOBILE, FIXED_LINE, VOIP, or TOLL_FREE derived from the region's numbering metadata. Be aware it can return FIXED_LINE_OR_MOBILE when a country's ranges don't distinguish the two, which is an honest either-or rather than a definite answer. Because this comes from static metadata and not a live network check, it reflects how the range was originally allocated and won't catch a number that has since been ported to a different carrier or line type.

Thomas Tsui

Founder of Boundstone — building phone, email and IP validation you can actually verify.

One honest API for email, phone and IP — every response lists what it checked and what it didn't claim to. Free tier: 250 credits/month, no card, credits never expire.

More from Boundstone — API documentation · Benchmark methodology · The benchmark series · Buyer's checklist