Credit Card Regex in Java: Brand Check and Luhn Validation

Validate credit card numbers in Java with a regex per card brand, brand detection through named groups and the Luhn checksum. Uses only published sandbox test numbers.

Java regex

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.

BrandStarts withLength
Visa413, 16 or 19
Mastercard51 to 55, or 2221 to 272016
American Express34 or 3715
Discover6011, 644 to 649, or 6516 to 19
Diners Club International300 to 305, 36 or 3814
JCB3528 to 358916 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:

GroupRegexHow it reads
visa4[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
amex3[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
diners3(?:0[0-5]|[68][0-9])[0-9]{11}300 to 305, 36x or 38x, then 11 digits
jcb35(?: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:

  1. Keep the rightmost digit (the check digit) as it is.
  2. Double every second digit, moving left. If the result is greater than 9, subtract 9.
  3. 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:

Digit378282246310005
Doubled?yesyesyesyesyesyesyes
Value358484286610005

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.

Happy Learning !!

Source Code on Github

Leave a Comment

Comments are closed.

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.