Credit card number validation in Java takes three checks: the input contains only digits and separators, the number has a prefix and length that a card network uses, and the last digit passes the Luhn checksum. A regex handles the first two checks and can also tell us the card brand. The Luhn checksum is arithmetic, so it needs a few lines of Java code.
This article builds a regex for Visa, Mastercard (including the 2-series numbers), American Express, Discover, Diners Club and JCB, brand detection with named groups, and a Luhn check, and then combines them into one method. It also fixes two problems in the pattern that earlier versions of this page used. All card numbers shown are the sandbox test numbers that Cybersource, a Visa company, publishes for developers; they cannot be charged.
Each line below was run on Java 25. CARD is the brand pattern from section 2, and the methods are defined in sections 3 to 6.
digitsOnly("4111 1111 1111 1111"); // Optional[4111111111111111]
CARD.matcher("4111111111111111").matches(); // true
brand("5555555555554444"); // Optional[mastercard]
brand("2222420000001113"); // Optional[mastercard], 2-series
brand("378282246310005"); // Optional[amex]
passesLuhn("4111111111111111"); // true
passesLuhn("4111111111111112"); // false
isValidCardNumber("4111-1111-1111-1111"); // true
1. Prefixes and Lengths by Card Brand
A card number (also called the PAN, primary account number) is 8 to 19 digits long. Its first digits, the issuer identification number (IIN), identify the network and the issuing bank, and the last digit is a check digit. The numbering follows the ISO/IEC 7812 standard.
The prefixes below come from the IIN table in the Wikipedia article on payment card numbers. Networks add ranges from time to time, so a brand regex needs a review whenever a network announces a new range.
| Brand | Starts with | Length |
|---|---|---|
| Visa | 4 | 13, 16 or 19 |
| Mastercard | 51 to 55, or 2221 to 2720 | 16 |
| American Express | 34 or 37 | 15 |
| Discover | 6011, 644 to 649, or 65 | 16 to 19 |
| Diners Club International | 300 to 305, 36 or 38 | 14 |
| JCB | 3528 to 3589 | 16 to 19 |
The Diners Club row covers the classic 14-digit numbers only. Wikipedia lists more prefixes and lengths for Diners Club, and in the US and Canada, Diners Club cards use Mastercard numbers.
2. A Regex for Each Card Brand
The pattern runs on digits only, after spaces and hyphens are removed (section 4). It has one alternative per brand, and each alternative is a named group, written (?<name>…), so that we can later ask which brand matched.
static final Pattern CARD = Pattern.compile(
"^(?:(?<visa>4[0-9]{12}(?:[0-9]{3}){0,2})"
+ "|(?<mastercard>(?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12})"
+ "|(?<amex>3[47][0-9]{13})"
+ "|(?<discover>(?:6011|64[4-9][0-9]|65[0-9]{2})[0-9]{12,15})"
+ "|(?<diners>3(?:0[0-5]|[68][0-9])[0-9]{11})"
+ "|(?<jcb>35(?:2[89]|[3-8][0-9])[0-9]{12,15}))$");
Each alternative encodes the prefix and the length from the table:
| Group | Regex | How it reads |
|---|---|---|
| visa | 4[0-9]{12}(?:[0-9]{3}){0,2} | 4, then 12 digits, then zero, one or two blocks of 3: 13, 16 or 19 digits |
| mastercard | (?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12} | A four-digit prefix from 5100 to 5599 or 2221 to 2720, then 12 digits |
| amex | 3[47][0-9]{13} | 34 or 37, then 13 digits |
| discover | (?:6011|64[4-9][0-9]|65[0-9]{2})[0-9]{12,15} | 6011, 644x to 649x or 65xx, then 12 to 15 digits |
| diners | 3(?:0[0-5]|[68][0-9])[0-9]{11} | 300 to 305, 36x or 38x, then 11 digits |
| jcb | 35(?:2[89]|[3-8][0-9])[0-9]{12,15} | 3528 to 3589, then 12 to 15 digits |
The Mastercard 2-series range is the hardest part, because a regex cannot compare numbers. A numeric range such as 2221 to 2720 must be split into pieces that each fit a character class: 2221 to 2229, 2230 to 2299, 2300 to 2699, 2700 to 2719 and 2720.
CARD.matcher("4622943127013705").matches(); // true, Visa
CARD.matcher("2222630000001125").matches(); // true, Mastercard 2-series
CARD.matcher("6011111111111117").matches(); // true, Discover
CARD.matcher("3566111111111113").matches(); // true, JCB
The project’s test class also checks the boundaries of every range, for example that prefix 2720 matches and 2721 does not.
3. Detecting the Card Brand
After a successful match, exactly one of the named groups holds text and the others are null. Since Java 20, Matcher.namedGroups() returns the group names of the pattern, so we can find the matching brand without listing the names a second time:
static Optional<String> brand(String digits) {
Matcher m = CARD.matcher(digits);
if (!m.matches()) {
return Optional.empty();
}
return m.namedGroups().keySet().stream()
.filter(name -> m.group(name) != null)
.findFirst();
}
brand("4111111111111111"); // Optional[visa]
brand("2222420000001113"); // Optional[mastercard]
brand("6011111111111117"); // Optional[discover]
brand("3566111111111113"); // Optional[jcb]
brand("0000000000000000"); // Optional.empty
The brand is useful for showing a card logo while the user types, or for rejecting brands a shop does not accept. To accept fewer brands, delete their alternatives from the pattern, and make sure no empty alternative is left behind: a stray | at the end of the group lets the empty string match.
3.1. What Was Wrong With the Old Pattern
The earlier version of this article used a widely copied pattern from the Regular Expressions Cookbook. It has two problems today:
- Its Mastercard branch knows only the 51 to 55 prefixes, so it rejects every 2-series card.
- Its Diners Club branch, 3(?:0[0-5]|[68][0-9])?[0-9]{11}, has a ? after the prefix group. That makes the prefix optional, so any 12-digit number that starts with 3 passes.
OLD_CARD.matcher("2222420000001113").matches(); // false, Mastercard 2-series rejected
CARD.matcher("2222420000001113").matches(); // true
OLD_CARD.matcher("312345678901").matches(); // true, 12 digits accepted as Diners
CARD.matcher("312345678901").matches(); // false
The old JCB branch also accepted the 2131 and 1800 prefixes, which the current IIN table no longer lists for JCB, and the Visa and Discover branches did not allow 19-digit numbers.
4. Cleaning the Input
Card numbers are printed in groups, and people type them with spaces or hyphens: “4111 1111 1111 1111”, or “3782 822463 10005” for an American Express card. We first check that the input contains only digits, spaces and hyphens, and then remove the separators:
static final Pattern ALLOWED_INPUT = Pattern.compile("^[0-9 -]+$");
static Optional<String> digitsOnly(String input) {
if (input == null || !ALLOWED_INPUT.matcher(input.strip()).matches()) {
return Optional.empty();
}
return Optional.of(input.replaceAll("[ -]", ""));
}
digitsOnly("4111-1111-1111-1111"); // Optional[4111111111111111]
digitsOnly(" 3782 822463 10005 "); // Optional[378282246310005]
digitsOnly("4111.1111.1111.1111"); // Optional.empty, dots
digitsOnly("4111 1111 1111 111O"); // Optional.empty, letter O
Rejecting other characters, instead of deleting everything that is not a digit, matters: removing letters from “4111 1111 1111 111O” would turn a typo into a different 15-digit number.
5. The Luhn Checksum in Java
The Luhn algorithm, also called the mod 10 algorithm, detects typing errors. A regex cannot check it, because the check digit depends on the sum of all the other digits. The rule, described in the Luhn algorithm article, works from right to left:
- Keep the rightmost digit (the check digit) as it is.
- Double every second digit, moving left. If the result is greater than 9, subtract 9.
- Add all the digits. The number is valid when the sum is divisible by 10.
For the American Express test number 378282246310005, the doubled digits are in the 2nd, 4th, 6th and later positions from the right:
| Digit | 3 | 7 | 8 | 2 | 8 | 2 | 2 | 4 | 6 | 3 | 1 | 0 | 0 | 0 | 5 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Doubled? | yes | yes | yes | yes | yes | yes | yes | ||||||||
| Value | 3 | 5 | 8 | 4 | 8 | 4 | 2 | 8 | 6 | 6 | 1 | 0 | 0 | 0 | 5 |
The values add up to 60, which is divisible by 10, so the number passes. The 7 became 5 because 7 x 2 = 14 and 14 – 9 = 5.
static boolean passesLuhn(String digits) {
int sum = 0;
boolean doubleIt = false;
for (int i = digits.length() - 1; i >= 0; i--) {
int d = digits.charAt(i) - '0';
if (doubleIt) {
d *= 2;
if (d > 9) {
d -= 9;
}
}
sum += d;
doubleIt = !doubleIt;
}
return sum % 10 == 0;
}
passesLuhn("378282246310005"); // true
passesLuhn("378282246310006"); // false, last digit changed
passesLuhn("4111111111111121"); // false, two digits swapped
passesLuhn("0000000000000000"); // true, but no brand matches
The Luhn check catches every single wrong digit and most swaps of two neighboring digits. The last line shows why it is not enough on its own: sixteen zeros pass the checksum, so the brand regex still has to run. ISBNs use a similar weighted checksum, shown in validating ISBN-10 and ISBN-13 in Java.
6. Putting the Checks Together
The complete validator chains the three steps with Optional: clean the input, match a brand pattern, then run the Luhn check.
static boolean isValidCardNumber(String input) {
return digitsOnly(input)
.filter(d -> CARD.matcher(d).matches())
.filter(CreditCardValidation::passesLuhn)
.isPresent();
}
isValidCardNumber("5555-5555-5555-4444"); // true
isValidCardNumber("6011111111111117"); // true
isValidCardNumber("4111111111111112"); // false, Luhn fails
isValidCardNumber("0000000000000000"); // false, no brand
isValidCardNumber("4111 1111"); // false, too short
A number that passes all three checks is well formed. It can still be expired, blocked, or belong to someone else; only an authorization request through a payment provider answers that. Client-side validation exists to catch typing errors early, before the payment request.
Card numbers are also regulated data. The PCI Security Standards Council sets the rules for storing and displaying them; in practice, most applications pass the number straight to a payment provider and never store it.
7. Running the Example
The credit card validation project on GitHub contains every method from this article. Running its main class prints every expression with the result, and the JUnit 6 tests check the published test numbers, the prefix boundaries of each range, the old pattern’s problems and a loop that changes each digit of a test number to confirm that Luhn detects it. Maven and a JDK 25 installation are enough to run it.
mvn -q compile exec:java
mvn test
8. Conclusion
Validating a credit card number is three separate checks. A character check and a separator cleanup handle user input, a regex with one named group per brand checks the prefix and length and tells us the brand, and the Luhn algorithm in a few lines of Java catches typing errors. Prefix ranges change over time, as the Mastercard 2-series showed, so the brand pattern needs an occasional review against the current IIN table. None of these checks proves that a card can be charged; that answer comes only from the payment provider.
9. References
The Cybersource testing guide is the source of the test card numbers; the Wikipedia articles list the IIN ranges and describe the Luhn algorithm.
- Cybersource Testing Guide: test card numbers
- Payment card number (Wikipedia)
- Luhn algorithm (Wikipedia)
- Matcher.namedGroups() (Java SE 25 API)
- PCI Security Standards Council
Happy Learning !!
What is the difference between () and []