Skip to content

IBAN Validator — Java source

Validate International Bank Account Numbers (IBAN) with the mod-97 checksum, verify the country-specific length, and format the result. 100% client-side, no network.

This is the Java implementation — the same logic the interactive tool runs, in a shareable, citable form.

// iban-validator — pure IBAN validation logic (ISO 13616 mod-97 checksum).
//
// Language: Java (Java 17, standard library only)
// Source:   CosmoDev polyglot showcase port of the iban-validator tool,
//           ported from src/lib/iban.ts (the canonical TypeScript
//           implementation).
// License:  display source — part of CosmoDev's polyglot tool pages
//
// Normalizes the input (uppercase, strip whitespace/dashes), checks the
// structure (country code + 2 check digits + 1-30 BBAN chars), verifies
// the per-country length, then runs the ISO 13616 mod-97 checksum: move
// the first 4 chars to the end, map A=10..Z=35, and confirm the
// resulting integer is congruent to 1 mod 97. The remainder is folded
// one decimal digit at a time (it stays < 97), so no BigInteger or
// regex machinery is required.

package org.cosmolabs.cosmodev.snippets;

import java.util.Locale;
import java.util.Map;

/** Pure IBAN validation logic — ISO 13616 mod-97 checksum. */
public final class IbanValidator {

    /** Per-country IBAN lengths (ISO 13616) — a representative subset. */
    private static final Map<String, Integer> IBAN_LENGTHS = Map.ofEntries(
            Map.entry("AL", 28), Map.entry("AD", 24), Map.entry("AT", 20),
            Map.entry("AZ", 28), Map.entry("BH", 22), Map.entry("BY", 28),
            Map.entry("BE", 16), Map.entry("BA", 20), Map.entry("BR", 29),
            Map.entry("BG", 22), Map.entry("CR", 22), Map.entry("HR", 21),
            Map.entry("CY", 28), Map.entry("CZ", 24), Map.entry("DK", 18),
            Map.entry("DO", 28), Map.entry("EE", 20), Map.entry("FO", 18),
            Map.entry("FI", 18), Map.entry("FR", 27), Map.entry("GE", 22),
            Map.entry("DE", 22), Map.entry("GI", 23), Map.entry("GR", 27),
            Map.entry("GL", 18), Map.entry("GT", 28), Map.entry("HU", 28),
            Map.entry("IS", 26), Map.entry("IE", 22), Map.entry("IL", 23),
            Map.entry("IT", 27), Map.entry("JO", 30), Map.entry("KZ", 20),
            Map.entry("XK", 20), Map.entry("KW", 30), Map.entry("LV", 21),
            Map.entry("LB", 28), Map.entry("LI", 21), Map.entry("LT", 20),
            Map.entry("LU", 20), Map.entry("MK", 19), Map.entry("MT", 31),
            Map.entry("MR", 27), Map.entry("MU", 30), Map.entry("MC", 27),
            Map.entry("MD", 24), Map.entry("ME", 22), Map.entry("NL", 18),
            Map.entry("NO", 15), Map.entry("PK", 24), Map.entry("PS", 29),
            Map.entry("PL", 28), Map.entry("PT", 25), Map.entry("QA", 29),
            Map.entry("RO", 24), Map.entry("LC", 32), Map.entry("SM", 27),
            Map.entry("ST", 25), Map.entry("SA", 24), Map.entry("RS", 22),
            Map.entry("SC", 31), Map.entry("SK", 24), Map.entry("SI", 19),
            Map.entry("SG", 19), Map.entry("ES", 24), Map.entry("SE", 24),
            Map.entry("CH", 21), Map.entry("TL", 23), Map.entry("TN", 24),
            Map.entry("TR", 26), Map.entry("UA", 29), Map.entry("AE", 23),
            Map.entry("GB", 22), Map.entry("VG", 24));

    /** Result of validation — nullable fields mirror the TS nullable fields. */
    public record IbanInfo(
            String input,
            String cleaned,
            String countryCode,     // null when the head is not 2 uppercase letters
            boolean valid,
            boolean checksumOk,
            boolean lengthOk,
            Integer expectedLength, // null when the country is unknown
            String formatted,
            String error) {         // null when valid
    }

    private IbanValidator() {
    }

    /** ISO 13616 mod-97 checksum over a CLEANED iban (uppercase, no spaces). */
    public static boolean mod97Check(String cleaned) {
        int n = cleaned.length();
        if (n < 4) {
            return false;
        }
        // Move the first 4 chars (country + check) to the end.
        String rearranged = cleaned.substring(4) + cleaned.substring(0, 4);
        int rem = 0;
        for (int i = 0; i < rearranged.length(); i++) {
            char ch = rearranged.charAt(i);
            int value;
            int digits;
            if (ch >= '0' && ch <= '9') {
                value = ch - '0';
                digits = 1;
            } else if (ch >= 'A' && ch <= 'Z') {
                value = ch - 'A' + 10; // A=10 .. Z=35
                digits = 2;
            } else {
                return false; // invalid character
            }
            // Fold one decimal digit at a time; rem stays < 97.
            if (digits == 2) {
                rem = (rem * 10 + value / 10) % 97;
            }
            rem = (rem * 10 + value % 10) % 97;
        }
        return rem == 1;
    }

    /**
     * Insert a space every 4 characters (last group may be short) — the
     * hand-rolled equivalent of the TS regex /(.{4})(?=.)/g + trim.
     */
    private static String formatGroups(String cleaned) {
        StringBuilder out = new StringBuilder(cleaned.length() + cleaned.length() / 4);
        for (int i = 0; i < cleaned.length(); i++) {
            if (i > 0 && i % 4 == 0) {
                out.append(' ');
            }
            out.append(cleaned.charAt(i));
        }
        return out.toString();
    }

    private static boolean isUpperAlpha(char c) {
        return c >= 'A' && c <= 'Z';
    }

    private static boolean isDigitChar(char c) {
        return c >= '0' && c <= '9';
    }

    /** Validate an IBAN. Never throws — all failures are reported via {@code error}. */
    public static IbanInfo validateIban(String input) {
        String src = (input == null) ? "" : input;

        // Uppercase and strip whitespace/dashes.
        StringBuilder cleanedSb = new StringBuilder();
        for (char ch : src.toUpperCase(Locale.ROOT).toCharArray()) {
            if (ch == ' ' || ch == '-' || ch == '\t' || ch == '\n' || ch == '\r') {
                continue;
            }
            cleanedSb.append(ch);
        }
        String cleaned = cleanedSb.toString();
        int n = cleaned.length();

        String cc = n >= 2 && isUpperAlpha(cleaned.charAt(0)) && isUpperAlpha(cleaned.charAt(1))
                ? cleaned.substring(0, 2)
                : null;
        Integer expected = (cc == null) ? null : IBAN_LENGTHS.get(cc);

        String formatted = formatGroups(cleaned);

        // Structure: 2 letters, 2 digits, then 1..30 alnum, total len 5..34.
        boolean structOk = n >= 5 && n <= 34
                && isUpperAlpha(cleaned.charAt(0))
                && isUpperAlpha(cleaned.charAt(1))
                && isDigitChar(cleaned.charAt(2))
                && isDigitChar(cleaned.charAt(3));
        if (structOk) {
            for (int i = 4; i < n; i++) {
                char ch = cleaned.charAt(i);
                if (!isUpperAlpha(ch) && !isDigitChar(ch)) {
                    structOk = false;
                    break;
                }
            }
        }
        if (!structOk) {
            return new IbanInfo(src, cleaned, cc, false, false, false, expected,
                    formatted, "Invalid IBAN format.");
        }

        boolean checksumOk = mod97Check(cleaned);
        boolean lengthOk = expected == null || n == expected;
        if (!lengthOk) {
            return new IbanInfo(src, cleaned, cc, false, checksumOk, false, expected,
                    formatted, "Length should be " + expected + " for " + cc + ".");
        }
        if (!checksumOk) {
            return new IbanInfo(src, cleaned, cc, false, false, lengthOk, expected,
                    formatted, "Checksum failed.");
        }
        return new IbanInfo(src, cleaned, cc, true, true, lengthOk, expected,
                formatted, null);
    }

    public static void main(String[] args) {
        // GB82 WEST 1234 5698 7654 32 — a known-good reference IBAN.
        IbanInfo r = validateIban("GB82WEST12345698765432");
        System.out.println("valid=" + r.valid()
                + " error=" + (r.error() == null ? "(none)" : r.error()));
    }
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →