IBAN Validation in Java: Code Examples & Best Practices
Three production-ready Java approaches: the REST API, Apache Commons Validator, and iban4j, with Spring Boot integration and JUnit 5 tests.
Founder, ibanchecker.cash
IBAN validation in Java can be approached three ways: a REST API call for results with bank metadata, the Apache Commons Validator library for offline structural checks, or the iban4j library for a dedicated IBAN type system. This guide covers all three with production-ready code examples, Spring Boot integration, and a complete unit test suite.
Why IBAN Validation Requires More Than a Regex
An IBAN has three validation layers that must all pass:
- Format: Two-letter country code, two check digits, alphanumeric BBAN.
- Length: Each country has a fixed IBAN length, Germany (DE) is always 22 characters, France (FR) always 27. A single extra or missing character is a hard error.
- MOD-97 check: ISO 13616 specifies that moving the first four characters to the end, replacing letters with numbers (A=10…Z=35), and computing the result modulo 97 must yield exactly 1. This catches all single-character errors and adjacent transpositions.
Approach 1: Validate via the ibanchecker.cash API
The ibanchecker.cash API returns structural validity, bank name, BIC, and country data in a single call, no library dependencies required.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class IbanApiValidator {
private static final String API_URL = "https://ibanchecker.cash/api/v1/validate";
private final HttpClient client = HttpClient.newHttpClient();
public String validate(String iban) throws Exception {
String body = "{\"iban\":\"" + iban.replace("\"", "") + "\"}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer YOUR_API_KEY")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = client.send(
request, HttpResponse.BodyHandlers.ofString()
);
return response.body();
}
public static void main(String[] args) throws Exception {
IbanApiValidator validator = new IbanApiValidator();
String result = validator.validate("DE89370400440532013000");
System.out.println(result);
// {"valid":true,"iban":"DE89 3704 0044 0532 0130 00",
// "country":"Germany","bankName":"Deutsche Bank","bic":"DEUTDEDB"}
}
}For bulk validation, use the /api/v1/validate/bulk endpoint with a JSON array of up to 100 IBANs:
String bulkBody = "{\"ibans\":[\"DE89370400440532013000\",\"GB29NWBK60161331926819\"]}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://ibanchecker.cash/api/v1/validate/bulk"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer YOUR_API_KEY")
.POST(HttpRequest.BodyPublishers.ofString(bulkBody))
.build();Approach 2: Apache Commons Validator
The Apache Commons Validator library (commons-validator) includes an IBANValidator class that validates format, length, and MOD-97 for all IBAN countries. Add the dependency:
<!-- Maven -->
<dependency>
<groupId>commons-validator</groupId>
<artifactId>commons-validator</artifactId>
<version>1.9.0</version>
</dependency>// Gradle
implementation 'commons-validator:commons-validator:1.9.0'Usage:
import org.apache.commons.validator.routines.IBANValidator;
public class CommonsIbanExample {
private static final IBANValidator validator = IBANValidator.getInstance();
public static boolean isValid(String iban) {
return validator.isValid(iban);
}
public static void main(String[] args) {
System.out.println(isValid("DE89370400440532013000")); // true
System.out.println(isValid("DE89370400440532013001")); // false, wrong check digit
System.out.println(isValid("GB29NWBK60161331926819")); // true
System.out.println(isValid("XX89370400440532013000")); // false, unknown country
}
}The Commons IBANValidator handles leading and trailing whitespace but does not strip internal spaces, normalize the input before calling isValid:
public static String normalize(String raw) {
if (raw == null) return "";
return raw.trim().replace(" ", "").replace("-", "").toUpperCase();
}
// Safe call pattern:
boolean valid = validator.isValid(normalize(userInput));Approach 3: iban4j Library
The iban4j library provides a typed Iban class that validates at construction time and provides structured access to IBAN components. It also includes BIC (SWIFT code) validation.
<!-- Maven -->
<dependency>
<groupId>org.iban4j</groupId>
<artifactId>iban4j</artifactId>
<version>3.2.10-RELEASE</version>
</dependency>import org.iban4j.Iban;
import org.iban4j.IbanFormatException;
import org.iban4j.InvalidCheckDigitException;
import org.iban4j.UnsupportedCountryException;
public class Iban4jExample {
public static void validateWithIban4j(String raw) {
try {
Iban iban = Iban.valueOf(raw.replace(" ", ""));
System.out.println("Country : " + iban.getCountryCode());
System.out.println("Bank code: " + iban.getBankCode());
System.out.println("Account : " + iban.getAccountNumber());
System.out.println("Formatted: " + iban.toFormattedString());
} catch (IbanFormatException e) {
System.err.println("Format error: " + e.getMessage());
} catch (InvalidCheckDigitException e) {
System.err.println("Check digit error: " + e.getMessage());
} catch (UnsupportedCountryException e) {
System.err.println("Unknown country: " + e.getMessage());
}
}
public static void main(String[] args) {
validateWithIban4j("DE89370400440532013000");
validateWithIban4j("GB29 NWBK 6016 1331 9268 19");
}
}iban4j also supports generating test IBANs, useful for unit tests:
import org.iban4j.CountryCode;
Iban testIban = new Iban.Builder()
.countryCode(CountryCode.DE)
.bankCode("37040044")
.accountNumber("0532013000")
.build();
System.out.println(testIban.toFormattedString());
// DE89 3704 0044 0532 0130 00Manual MOD-97 Implementation
When you need no external dependencies, for embedded systems, Android apps, or compliance environments with restricted libraries, implement the ISO 13616 algorithm directly:
import java.util.Map;
public class ManualIbanValidator {
private static final Map<String, Integer> IBAN_LENGTHS = Map.ofEntries(
Map.entry("AD", 24), Map.entry("AE", 23), Map.entry("AT", 20),
Map.entry("BE", 16), Map.entry("BG", 22), Map.entry("BH", 22),
Map.entry("CH", 21), Map.entry("CY", 28), Map.entry("CZ", 24),
Map.entry("DE", 22), Map.entry("DK", 18), Map.entry("EE", 20),
Map.entry("ES", 24), Map.entry("FI", 18), Map.entry("FR", 27),
Map.entry("GB", 22), Map.entry("GR", 27), Map.entry("HR", 21),
Map.entry("HU", 28), Map.entry("IE", 22), Map.entry("IS", 26),
Map.entry("IT", 27), Map.entry("LI", 21), Map.entry("LT", 20),
Map.entry("LU", 20), Map.entry("LV", 21), Map.entry("MC", 27),
Map.entry("MT", 31), Map.entry("NL", 18), Map.entry("NO", 15),
Map.entry("PL", 28), Map.entry("PT", 25), Map.entry("RO", 24),
Map.entry("SA", 24), Map.entry("SE", 24), Map.entry("SI", 19),
Map.entry("SK", 24), Map.entry("TR", 26), Map.entry("UA", 29)
);
public static String normalize(String raw) {
if (raw == null) return "";
return raw.trim().replace(" ", "").replace("-", "").toUpperCase();
}
public static boolean validate(String raw) {
String iban = normalize(raw);
if (!iban.matches("^[A-Z]{2}[0-9]{2}[A-Z0-9]+$")) return false;
String country = iban.substring(0, 2);
Integer expectedLength = IBAN_LENGTHS.get(country);
if (expectedLength == null || iban.length() != expectedLength) return false;
String rearranged = iban.substring(4) + iban.substring(0, 4);
StringBuilder numeric = new StringBuilder();
for (char c : rearranged.toCharArray()) {
if (Character.isLetter(c)) {
numeric.append(c - 'A' + 10);
} else {
numeric.append(c);
}
}
int remainder = 0;
for (char c : numeric.toString().toCharArray()) {
remainder = (remainder * 10 + (c - '0')) % 97;
}
return remainder == 1;
}
}The digit-by-digit remainder computation avoids BigInteger, the intermediate value never exceeds two digits, keeping arithmetic within int range throughout.
Spring Boot Integration
For Spring Boot REST APIs, add IBAN validation as a custom ConstraintValidator so it integrates with Bean Validation:
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = IbanConstraintValidator.class)
public @interface ValidIban {
String message() default "Invalid IBAN";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
public class IbanConstraintValidator
implements ConstraintValidator<ValidIban, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext ctx) {
if (value == null || value.isBlank()) return false;
return ManualIbanValidator.validate(value);
}
}// Usage in a DTO:
public record PaymentRequest(
@ValidIban String iban,
String beneficiaryName,
java.math.BigDecimal amount
) {}
// Controller:
@RestController
public class PaymentController {
@PostMapping("/payments")
public ResponseEntity<Void> create(@Valid @RequestBody PaymentRequest req) {
// iban is guaranteed valid here
return ResponseEntity.ok().build();
}
}Unit Tests with JUnit 5
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;
class ManualIbanValidatorTest {
@ParameterizedTest
@ValueSource(strings = {
"DE89370400440532013000",
"GB29NWBK60161331926819",
"FR7630006000011234567890189",
"NL91ABNA0417164300",
"DE89 3704 0044 0532 0130 00", // with spaces
"de89370400440532013000", // lowercase
})
void testValidIbans(String iban) {
assertTrue(ManualIbanValidator.validate(iban));
}
@ParameterizedTest
@ValueSource(strings = {
"DE89370400440532013001", // wrong check digit
"DE8937040044053201300", // too short
"XX89370400440532013000", // unknown country
"",
"not-an-iban",
})
void testInvalidIbans(String iban) {
assertFalse(ManualIbanValidator.validate(iban));
}
@Test
void testNormalizesSpaces() {
assertTrue(ManualIbanValidator.validate("DE89 3704 0044 0532 0130 00"));
}
@Test
void testNormalizesLowercase() {
assertTrue(ManualIbanValidator.validate("de89370400440532013000"));
}
}Which Approach to Use?
Use the ibanchecker.cash API when you need bank name, BIC, or country metadata alongside validity. Use Apache Commons Validator or iban4j for offline-first services or high-throughput pipelines where external calls are not acceptable. Use the manual implementation when you need zero runtime dependencies.
For most Spring Boot services processing payments in a network-connected environment, the API is the correct default, it keeps your library dependencies minimal and gets you bank metadata that the local libraries cannot provide.
Sources & References
- SWIFT IBAN Registry: Official IBAN country format specifications
- ISO 13616-1:2020, International standard for IBAN structure
- iban4j (Maven): Java library for IBAN and BIC validation
- ISO 7064: MOD-97-10 check digit algorithm standard
Last updated: June 2026
Integrate IBAN validation via API
REST API with 100 free validations a month. JSON response with bank name, SEPA status, and BBAN breakdown.
Introducing Northern Cyprus UBAN Support: Validator and Bank Directory
A validator for the 28-character Northern Cyprus UBAN, plus a directory of all 23 registered banks. What UBAN is, and wh...
One Client, Four Registries: PyPI, npm, Packagist and MCP All Disagree
Why OIDC cannot do npm's first publish, why Packagist needs no workflow, and the PyPI name check that returns 200 for na...
npm trust github Fails with 400 Bad Request: The Missing Permission Flag
A bare 400 with no error body, caused by a 20 May 2026 registry rule. The exact npm version boundary, the one flag that ...
How to Validate IBANs in JavaScript (API & Manual)
Two approaches: call the ibanchecker.cash API for full bank data, or implement MOD-97 validation yourself. Includes work...