SSN Regex in Java: Validate Social Security Numbers

SSN regex for Java that rejects the numbers the SSA never assigns, accepts spaces or no dashes with a backreference, and masks SSNs in logs.

SSN 123-45-6789 split into area, group and serial number, with the values the SSA never assigns and the regex part for each

A US Social Security number (SSN) is a nine-digit number written as AAA-GG-SSSS, with a three-digit area number, a two-digit group number and a four-digit serial number, and the SSN regex ^(?!000|666)[0-8][0-9]{2}-(?!00)[0-9]{2}-(?!0000)[0-9]{4}$ validates both its shape and the values the SSA never assigns. A shape-only pattern accepts “000-00-0000”, which is the first thing people type to get past a required field.

We use this SSN regex in payroll, tax and onboarding forms, and a variant of it to find and mask SSNs in log lines and support tickets. Every SSN in this article is made up, and “123-45-6789” and the numbers derived from it are placeholders, not anyone’s real number.

The following example validates, normalizes and masks SSNs with the methods of the class Ssns, which section 3 to section 7 build piece by piece.

boolean ok = Ssns.isValid("123-45-6789");             // true
boolean area000 = Ssns.isValid("000-45-6789");        // false, area 000
boolean area666 = Ssns.isValid("666-45-6789");        // false, area 666
boolean area900 = Ssns.isValid("900-45-6789");        // false, area 900-999
boolean group00 = Ssns.isValid("123-00-6789");        // false, group 00
boolean serial0 = Ssns.isValid("123-45-0000");        // false, serial 0000

Optional<String> spaced = Ssns.normalize("123 45 6789");      // Optional[123-45-6789]
String masked = Ssns.mask("SSN 123-45-6789 on file");         // "SSN ***-**-6789 on file"

Notice that each rejected number has the right shape and fails on one SSA rule. We start with the SSA rules, look at why a shape-only regex is not enough, explain the full regex, write the same rules without lookaheads, accept other separators, and finish with masking. Lookaheads and the other syntax are explained in our Java regex tutorial.

1. Which SSNs the SSA Never Assigns

The SSA operations manual section RM 10201.035 Invalid Social Security Numbers defines an invalid SSN as one the agency never assigned. It lists one rule for each part of the number.

PartDigitsNever assigned
Area number1 to 3000, 666, and 900 to 999
Group number4 and 500
Serial number6 to 90000
SSN 123-45-6789 split into area, group and serial number, with the values the SSA never assigns and the regex part for each
Each part of an SSN has its own forbidden values, and one negative lookahead per part rejects them

Older articles describe more rules, such as area numbers tied to states or a “highest group” per area. Those rules stopped applying on June 25, 2011, when the SSA started assigning SSNs randomly. Since that date, any area number outside the excluded ranges can be issued, so the three rules in the table are all that a format check can enforce.

2. Why a Shape-Only Regex Is Not Enough

The simplest SSN regex checks only the digit counts and the hyphens.

Pattern shapeOnly = Pattern.compile("^[0-9]{3}-[0-9]{2}-[0-9]{4}$");

boolean placeholder = shapeOnly.matcher("123-45-6789").matches();   // true
boolean zeros = shapeOnly.matcher("000-00-0000").matches();         // true, but never assigned
boolean area666 = shapeOnly.matcher("666-45-6789").matches();       // true, but never assigned

Say an HR onboarding form requires an SSN, and a new hire who does not want to share it types “000-00-0000”. A form that accepts it stores a number the SSA has never issued, and payroll finds out only when the SSA flags the first wage report. The regex in the next section rejects it at no extra cost.

3. The SSN Regex Explained

The full pattern adds three negative lookaheads and narrows the first digit. A negative lookahead, written (?!…), succeeds only when the text at the current position does not match its content, and it consumes no characters. So (?!00)[0-9]{2} means “two digits, but not 00”.

PartMeaning
^Start of the input
(?!000|666)The next characters are not “000” and not “666”
[0-8][0-9]{2}Three digits, where the first is 0 to 8, which excludes 900 to 999
–A hyphen
(?!00)[0-9]{2}Two digits, not “00”
–A hyphen
(?!0000)[0-9]{4}Four digits, not “0000”
$End of the input

The class Ssns holds this pattern and every other pattern and method of this article. The example project on GitHub contains the same class with JUnit tests for the sample table.

final class Ssns {
    static final Pattern SSN =
        Pattern.compile("^(?!000|666)[0-8][0-9]{2}-(?!00)[0-9]{2}-(?!0000)[0-9]{4}$");
    static final Pattern SSN_PARTS =
        Pattern.compile("(?<area>[0-9]{3})-(?<group>[0-9]{2})-(?<serial>[0-9]{4})");
    static final Pattern SSN_FLEXIBLE = Pattern.compile(
        "^(?!000|666)[0-8][0-9]{2}([- ]?)(?!00)[0-9]{2}\\1(?!0000)[0-9]{4}$");
    static final Pattern SSN_IN_TEXT = Pattern.compile("\\b[0-9]{3}-[0-9]{2}-([0-9]{4})\\b");

    static boolean isValid(String input) {
        return input != null && SSN.matcher(input).matches();
    }

    static boolean isValidWithoutLookahead(String input) {
        if (input == null) {
            return false;
        }
        Matcher m = SSN_PARTS.matcher(input);
        if (!m.matches()) {
            return false;
        }
        int area = Integer.parseInt(m.group("area"));
        int group = Integer.parseInt(m.group("group"));
        int serial = Integer.parseInt(m.group("serial"));
        return area != 0 && area != 666 && area < 900 && group != 0 && serial != 0;
    }

    static Optional<String> normalize(String input) {
        if (input == null) {
            return Optional.empty();
        }
        String trimmed = input.strip();
        if (!SSN_FLEXIBLE.matcher(trimmed).matches()) {
            return Optional.empty();
        }
        String digits = trimmed.replaceAll("[^0-9]", "");
        return Optional.of(digits.substring(0, 3) + "-" + digits.substring(3, 5) + "-" + digits.substring(5));
    }

    static String mask(String text) {
        return SSN_IN_TEXT.matcher(text).replaceAll("***-**-$1");
    }
}

The lookahead checks the three characters at its position, not the whole area number by value. That is fine here because the area always has three digits, so “066-45-6789” and “660-45-6789” pass, and only an area of “666” fails.

The made-up inputs in the table cover each rule and the common format mistakes.

InputResultReason
“123-45-6789”truePlaceholder that follows every rule
“001-45-6789”trueLowest allowed area
“899-45-6789”trueHighest allowed area
“123-01-6789”trueLowest allowed group
“123-45-0001”trueLowest allowed serial
“000-45-6789”falseArea 000
“666-45-6789”falseArea 666
“900-45-6789”falseArea in the 900 range
“999-45-6789”falseArea in the 900 range
“123-00-6789”falseGroup 00
“123-45-0000”falseSerial 0000
“123456789”falseNo hyphens (see section 5)
“12-345-6789”falseHyphens in the wrong places
“123-45-67890”falseTen digits
“abc-de-fghi”falseLetters
boolean lowArea = Ssns.isValid("001-45-6789");      // true
boolean highArea = Ssns.isValid("899-45-6789");     // true
boolean area066 = Ssns.isValid("066-45-6789");      // true
boolean noHyphen = Ssns.isValid("123456789");       // false
boolean tenDigits = Ssns.isValid("123-45-67890");   // false
boolean missing = Ssns.isValid(null);               // false

4. The Same Rules Without Lookaheads

Lookaheads are compact, but not every reader of the code knows them. The method isValidWithoutLookahead() uses a simple regex with named groups to split the number and checks the values with ordinary comparisons, which also lets an error message name the part that failed.

boolean high = Ssns.isValidWithoutLookahead("899-45-6789");   // true
boolean area666 = Ssns.isValidWithoutLookahead("666-45-6789"); // false
boolean group00 = Ssns.isValidWithoutLookahead("123-00-6789"); // false

The version without lookaheads is longer, but each SSA rule is one readable condition. The test class runs both versions on every area number from 000 to 999, combined with the boundary groups and serials, and confirms that they always agree. We pick whichever style the team finds easier to maintain.

5. Accepting Spaces or No Separator

People also type SSNs as “123 45 6789” or “123456789”. Making each hyphen optional with -? would also accept a mix such as “123-45 6789”. A backreference prevents that, because the pattern SSN_FLEXIBLE captures the separator in group 1 with ([- ]?), and \1 later requires the same text again.

Optional<String> spaces = Ssns.normalize("123 45 6789");    // Optional[123-45-6789]
Optional<String> none = Ssns.normalize("123456789");        // Optional[123-45-6789]
Optional<String> mixed = Ssns.normalize("123-45 6789");     // Optional.empty, mixed separators
Optional<String> mixed2 = Ssns.normalize("12345-6789");     // Optional.empty, mixed separators
Optional<String> area000 = Ssns.normalize("000456789");     // Optional.empty, area 000

When the first separator is empty, \1 matches an empty string, so “123456789” passes. After normalization, every stored SSN has the AAA-GG-SSSS form and passes the strict regex from section 3.

6. What the Regex Cannot Tell Us

A number that passes these rules is possible, not real. The regex cannot tell whether the SSA has issued the number, or whether it belongs to the person who entered it. Those checks need SSA services, for example the Social Security Number Verification Service that employers use to match names and SSNs for wage reporting.

Two related points come up in practice.

  • An Individual Taxpayer Identification Number (ITIN) is a nine-digit number that the IRS issues to people who need a taxpayer number but cannot get an SSN. ITINs start with 9, so a form that must accept both needs a separate rule for ITINs.
  • An SSN is sensitive personal data. We validate it, store it encrypted and keep it out of logs and error messages.

7. Finding and Masking SSNs in Text

The last point leads to a common task, which is finding SSN-like numbers in free text, such as log lines or support tickets, and masking them. With a word boundary \b on both ends, the pattern SSN_IN_TEXT does not match inside longer numbers, and the capture group keeps the last four digits for the replacement ***-**-$1, where $1 inserts group 1.

String log = "user=lokesh ssn=123-45-6789 ref=123-45-6780 order=1234-56-7890";

List<String> found = Ssns.SSN_IN_TEXT.matcher(log).results().map(MatchResult::group).toList();   // [123-45-6789, 123-45-6780]
String safeLog = Ssns.mask(log);                     // "user=lokesh ssn=***-**-6789 ref=***-**-6780 order=1234-56-7890"
String invalid = Ssns.mask("id 000-12-3456");        // "id ***-**-3456"

For masking, we use the shape-only pattern on purpose, because an invalid SSN such as “000-12-3456” in a log is still something we do not want to show. The order number “1234-56-7890” stays as it is because its first block has four digits. The article on word boundaries in Java regex explains \b in more detail.

8. SSN Regex FAQs

Payroll and HR forms raise these questions about SSN input.

8.1. Can an SSN Start With 9?

No. The SSA never assigns area numbers from 900 to 999. Numbers that start with 9 are ITINs or other IRS numbers, which is why [0-8] starts the area part of the regex.

8.2. How Do We Accept an SSN With or Without Dashes?

We capture the separator once and repeat it with a backreference, as in the pattern SSN_FLEXIBLE from section 5. The pattern accepts “123-45-6789”, “123 45 6789” and “123456789”, and rejects mixed separators.

8.3. Is 123-45-6789 a Valid SSN?

The number 123-45-6789 passes every format rule, but it is the best-known placeholder SSN, so people type it to get past a required field. A format check cannot detect such placeholder numbers, so systems that must reject them keep a short block list.

8.4. How Do We Show Only the Last Four Digits of an SSN?

We replace the first five digits and keep the serial number in a group, as mask() does in section 7.

String display = "123-45-6789".replaceAll("^\\d{3}-\\d{2}", "***-**");   // "***-**-6789"

8.5. Does the Regex Reject SSNs Issued Before 2011?

No. Numbers issued before the randomization follow the same three rules, so the regex accepts them. The randomization only removed the old ties between area numbers and states.

9. Conclusion

The SSA never assigns area numbers 000, 666 and 900 to 999, group number 00 or serial number 0000. The regex ^(?!000|666)[0-8][0-9]{2}-(?!00)[0-9]{2}-(?!0000)[0-9]{4}$ enforces all of these rules with three negative lookaheads, and a short Java method with named groups does the same in a more explicit way.

A backreference accepts spaces or no separators without allowing a mix. Since the 2011 randomization, there are no further format rules to check, and only the SSA can confirm that a number was issued to a given person.

10. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.