A US Social Security number (SSN) is a nine-digit number written as AAA-GG-SSSS: a three-digit area number, a two-digit group number and a four-digit serial number. The Social Security Administration (SSA) never assigns some values in each part, so a useful SSN regex checks those rules as well as the shape. In Java, the pattern ^(?!000|666)[0-8][0-9]{2}-(?!00)[0-9]{2}-(?!0000)[0-9]{4}$ does both.
We will go through the SSA rules for invalid SSNs, the regex that enforces them with negative lookaheads, the same rules in plain Java, input with spaces or no separators, and masking SSNs in log text. Every SSN in this article is made up; “123-45-6789” and the numbers derived from it are placeholders, not anyone’s real number.
The results below were printed by the example project on Java 25. isValidSsn() wraps the regex, and the other two methods appear in sections 5 and 7.
Pattern SSN = Pattern.compile("^(?!000|666)[0-8][0-9]{2}-(?!00)[0-9]{2}-(?!0000)[0-9]{4}$");
isValidSsn("123-45-6789"); // true
isValidSsn("000-45-6789"); // false, area 000
isValidSsn("666-45-6789"); // false, area 666
isValidSsn("900-45-6789"); // false, area 900-999
isValidSsn("123-00-6789"); // false, group 00
isValidSsn("123-45-0000"); // false, serial 0000
normalizeSsn("123 45 6789"); // Optional[123-45-6789]
maskSsns("SSN 123-45-6789 on file"); // "SSN ***-**-6789 on file"
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 three rules, one for each part of the number:
| Part | Digits | Never assigned |
|---|---|---|
| Area number | 1 to 3 | 000, 666, and 900 to 999 |
| Group number | 4 and 5 | 00 |
| Serial number | 6 to 9 | 0000 |
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, as its page on SSN randomization explains. Since then, any area number outside the excluded ranges can be issued, so the three rules above 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 SSN_SHAPE = Pattern.compile("^[0-9]{3}-[0-9]{2}-[0-9]{4}$");
SSN_SHAPE.matcher("123-45-6789").matches(); // true
SSN_SHAPE.matcher("000-00-0000").matches(); // true, but never assigned
SSN_SHAPE.matcher("666-45-6789").matches(); // true, but never assigned
“000-00-0000” is exactly the kind of value a user types to get past a required field. A form that accepts it stores a number the SSA has never issued. 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”.
| Part | Meaning |
|---|---|
| ^ | Start of the input |
| (?!000|666) | The next characters are not “000” and not “666” |
| [0-8][0-9]{2} | Three digits; 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 |
static final Pattern SSN =
Pattern.compile("^(?!000|666)[0-8][0-9]{2}-(?!00)[0-9]{2}-(?!0000)[0-9]{4}$");
static boolean isValidSsn(String input) {
return input != null && SSN.matcher(input).matches();
}
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 exactly three digits: “066-45-6789” and “660-45-6789” pass, and only an area of exactly “666” fails.
These made-up inputs cover each rule and the common format mistakes:
| Input | Result | Reason |
|---|---|---|
| “123-45-6789” | true | Placeholder that follows every rule |
| “001-45-6789” | true | Lowest allowed area |
| “899-45-6789” | true | Highest allowed area |
| “123-01-6789” | true | Lowest allowed group |
| “123-45-0001” | true | Lowest allowed serial |
| “000-45-6789” | false | Area 000 |
| “666-45-6789” | false | Area 666 |
| “900-45-6789” | false | Area in the 900 range |
| “999-45-6789” | false | Area in the 900 range |
| “123-00-6789” | false | Group 00 |
| “123-45-0000” | false | Serial 0000 |
| “123456789” | false | No hyphens (see section 5) |
| “12-345-6789” | false | Hyphens in the wrong places |
| “123-45-67890” | false | Ten digits |
| “abc-de-fghi” | false | Letters |
4. The Same Rules Without Lookaheads
Lookaheads are compact but not every reader of the code knows them. An alternative uses a simple regex with named groups to split the number, then checks the values with ordinary comparisons:
static final Pattern SSN_PARTS =
Pattern.compile("(?<area>[0-9]{3})-(?<group>[0-9]{2})-(?<serial>[0-9]{4})");
static boolean isValidSsnWithoutLookahead(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;
}
isValidSsnWithoutLookahead("899-45-6789"); // true
isValidSsnWithoutLookahead("666-45-6789"); // false
This version is longer, but each SSA rule is one readable condition, and an error message can name the part that failed. 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: the separator is captured in group 1 with ([- ]?), and \1 later requires the same text again.
static final Pattern SSN_FLEXIBLE = Pattern.compile(
"^(?!000|666)[0-8][0-9]{2}([- ]?)(?!00)[0-9]{2}\\1(?!0000)[0-9]{4}$");
static Optional<String> normalizeSsn(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));
}
normalizeSsn("123 45 6789"); // Optional[123-45-6789]
normalizeSsn("123456789"); // Optional[123-45-6789]
normalizeSsn("123-45 6789"); // Optional.empty, mixed separators
normalizeSsn("12345-6789"); // Optional.empty, mixed separators
normalizeSsn("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. If a form must accept both, it needs a separate rule for ITINs, because the SSN regex rejects area numbers from 900 to 999.
- 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: 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 does not match inside longer numbers, and the capture group keeps the last four digits for the replacement ***-**-$1, where $1 inserts group 1.
static final Pattern SSN_IN_TEXT = Pattern.compile("\\b[0-9]{3}-[0-9]{2}-([0-9]{4})\\b");
static String maskSsns(String text) {
return SSN_IN_TEXT.matcher(text).replaceAll("***-**-$1");
}
String log = "user=lokesh ssn=123-45-6789 ref=123-45-6780 order=1234-56-7890";
SSN_IN_TEXT.matcher(log).results().map(MatchResult::group).toList();
// [123-45-6789, 123-45-6780]
maskSsns(log);
// "user=lokesh ssn=***-**-6789 ref=***-**-6780 order=1234-56-7890"
maskSsns("id 000-12-3456"); // "id ***-**-3456"
For masking, we use the shape-only pattern on purpose: 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” is left alone because its first block has four digits. The article on word boundaries in Java regex explains \b in more detail.
8. Get the SSN Example Code
The SSN validation project on GitHub contains the class SsnValidation with every pattern and method from this article, and a JUnit 6 test class that checks the sample table, the normalization, the masking and the agreement of the two validators. The build uses Maven and Java 25.
mvn -q compile exec:java
mvn test
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
The SSA documents are the primary source for the invalid number rules and the randomization change.
- SSA POMS RM 10201.035: Invalid Social Security Numbers
- SSA: Social Security Number Randomization
- SSA: Social Security Number Verification Service
- Pattern (Java SE 25 API)
- Matcher.replaceAll() (Java SE 25 API)
Happy Learning !!