
Validates and formats IBANs and exposes country-specific details (bank/branch identifiers, SEPA/SWIFT registry data), with a non-throwing Result-based API and zero dependencies beyond the Kotlin stdlib. Supports all Kotlin Multiplatform targets.
This Kotlin Multiplatform library is a continuation and re-implementation of the original java-iban library by Barend Garvelink. It delivers IBAN validation, formatting, and country-specific IBAN details. The library is aimed to fulfill the same features as the original but in a Kotlin Multiplatform environment.
API stability. Kiban is pre-1.0 and the API is still being settled, so the 0.x line still breaks compatibility where the 1.0 freeze would otherwise lock in a mistake, and removals land without a deprecation cycle. What that becomes at 1.0 — the semver contract, the binary compatibility guarantee and the deprecation cycle for post-1.0 removals — is written down in VERSIONING.md.
The original java-iban library laid a solid foundation for IBAN validation and utility functions in Java environments. This library reimagines those capabilities with Kotlin's cross-platform features, making it ready for use on multiple platforms such as JVM, Android, iOS, and more.
Artifacts are published to Maven Central.
dependencies {
implementation("nl.bijdorpstudio.kiban:kiban:0.5.0")
}In a multiplatform project, add it to commonMain:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("nl.bijdorpstudio.kiban:kiban:0.5.0")
}
}
}Supported targets: JVM, Android, js (Node.js and browser), wasmJs (Node.js and browser), iOS,
macOS (Apple silicon only), watchOS, tvOS, linuxX64, linuxArm64, and mingwX64.
The js and wasmJs artifacts serve Kotlin/JS and Kotlin/Wasm consumers, not plain
JavaScript or TypeScript ones. Nothing in the library is annotated @JsExport, so none of its
declarations are reachable from hand-written JavaScript, and no TypeScript definitions are
generated for them; the artifacts are published to Maven Central as Kotlin klibs for a Gradle
build to resolve, not to npm. That is a deliberate scope boundary rather than an omission:
@JsExport accepts only a subset of Kotlin types, and once names, overloads and nullability are
visible to JavaScript they become a second frozen contract to maintain alongside the Kotlin one.
Exporting to plain JS stays possible as a purely additive change after 1.0 if there is demand for
it — withdrawing it again would not be.
| Requirement | |
|---|---|
| Kotlin |
2.4.0 or newer, for both the consumer's compiler and its apiVersion
|
| Java | bytecode level 17, so a JDK 17 or newer runtime |
| Android |
minSdk 24
|
| macOS |
Apple silicon only — the macOS artifact and the macOS slice of the Kiban XCFramework have been macosArm64 since 0.5.0 |
These are part of the compatibility contract rather than incidental facts about the current build:
raising any of them breaks consumers who cannot follow, so it only happens in a major release. The
Kotlin and Java floors are pinned by the tapmoc plugin, independently of the compiler the library
is built with, which moves forward on its own schedule. See VERSIONING.md.
The Kotlin floor is 2.4.0 so that an optional parameter can be added to a published function
without breaking binary compatibility: @IntroducedAt and ExperimentalVersionOverloading do not
resolve below a 2.4 languageVersion. It moved up from 2.3.0 before the 1.0 freeze deliberately,
since afterwards the same move would cost a major release. A second constraint keeps it from ever
going below 2.3: CountryCodes.lastUpdateDate returns kotlin.time.Instant, which the standard
library only makes non-experimental from 2.3, so a lower floor would ask callers for
@OptIn(kotlin.time.ExperimentalTime::class) to read a frozen public property. See
docs/144-instant-api-stability.md.
Parsing is strict: invalid input throws a typed IbanParseException, so you don't need to unwrap a
Result for the common case. The exception-free toIbanOrNull() and isValidIban() are there for
when you want to check input without paying for a stack trace.
Input must be ASCII. ISO 13616 defines the IBAN character set as A-Z0-9, so upper case ASCII
letters, ASCII digits and (ASCII 0x20) spaces are all that parse; non-ASCII look-alikes such as
fullwidth 9 or Arabic-Indic ٩ digits are rejected rather than normalized, because silently
rewriting a bank account identifier hides upstream data corruption. NFKC-normalize user input before
parsing if your input layer can produce them.
The whitespace leniency is just as narrow: the (ASCII 0x20) space is ignored between the first and
last character, so both "NL91ABNA0417164300" and "NL91 ABNA 0417 1643 00" parse, but a leading
or trailing space is rejected and so is any other whitespace anywhere — a tab or a non-breaking
space mid-IBAN is a paste artifact, not grouping, and is reported as an invalid character. Trim
before parsing if your input layer can produce them.
// The primary entry point. Throws IbanParseException on invalid input.
val iban: Iban = Iban( "NL91ABNA0417164300" )
// Or use the String extension; same throwing behaviour.
val parsed: Iban = "NL91ABNA0417164300".toIban()
// Exception-free fast paths.
val orNull: Iban? = "NL91ABNA0417164301".toIbanOrNull() // null, check digits are wrong
val isValid: Boolean = "NL91ABNA0417164300".isValidIban() // true
// Failures carry a typed reason, so you never have to match on messages.
try {
Iban( input )
} catch ( failure: IbanParseException ) {
when ( failure ) {
is IbanParseException.UnknownCountryCode -> reportUnknown( failure.countryCode )
is IbanParseException.WrongLength -> reportLength( failure.expectedLength, failure.actualLength )
is IbanParseException.WrongChecksum -> reportChecksum()
is IbanParseException.Malformed -> reportMalformed( failure.kind )
}
}
// toString() emits standard formatting, plain is compact.
val formatted = iban.toString() // "NL91 ABNA 0417 1643 00"
val plain = iban.plain // "NL91ABNA0417164300"
// Input may be formatted.
val anotherIban = Iban( "BE68 5390 0754 7034" )
// Iban implements Comparable<T>.
val ibans = getListOfIBANs()
ibans.sorted() // sorts in lexical order
// The equals() and hashCode() methods are implemented.
val ibansAsKeys = mutableMapOf<Iban, String>()
ibansAsKeys.put( iban, "this is fine" )
// You can use the Modulo97 class directly to compute or verify the check digits on an input.
val candidate = "GB29 NWBK 6016 1331 9268 19"
val valid = Modulo97.verifyCheckDigits( candidate ) // true
// Compose the IBAN for a country and BBAN; also throws on invalid input.
Iban.compose( "BI", "10000100010000332045181" ) // BI4210000100010000332045181
// You can query whether an IBAN is of a SEPA-participating country
val isSepa = Iban( candidate ).isSEPA // true
// You can query whether an IBAN is in the SWIFT Registry
val isRegistered = Iban( candidate ).isInSwiftRegistry // true
// Modulo97 API methods take CharSequence, not just String.
val builder = StringBuilder( "LU000019400644750000" )
val checkDigits = Modulo97.calculateCheckDigits( builder ) // 28
// Modulo97 API can calculate check digits, also for non-iban inputs.
// It does assume/require that the check digits are on indices 2 and 3.
Modulo97.calculateCheckDigits( "GB", "NWBK60161331926819" ) // 29
Modulo97.calculateCheckDigits( "XX", "X" ) // 72
// Get the expected IBAN length for a country code:
val expectedLength: Int? = CountryCodes.ibanLength( "DK" ) // 18
// Get the Bank Identifier and Branch Identifier:
val bankId: String? = iban.bankIdentifier
val branchId: String? = iban.branchIdentifier
// Get the BBAN, the counterpart of what Iban.compose() takes:
val bban: String = iban.bban // round trips: Iban.compose( iban.countryCode, iban.bban )Modulo97 is the one part of the library that still throws unconditionally: its inputs are
programmer-supplied, so a bad one is a contract violation rather than user input to be validated.
Iban's throwing entry points (Iban(...), Iban.compose(...), String.toIban()) are annotated
@Throws(IbanParseException::class), so Kotlin/Native's Objective-C exporter emits an NSError**
out-parameter and Swift sees a normal throws function instead of the process aborting on an
unannotated exception.
Iban(input) is operator fun invoke on the companion object, which only Kotlin has call syntax
for: Java reads it as Iban.Companion.invoke(...) and Swift as Iban.companion.invoke(input:).
For those callers there is Iban.parse(input), a named alias that parses identically, and
Iban.compose(...) is @JvmStatic for the same reason. Kotlin callers should keep using
Iban(input).
Iban(...), Iban.parse(...), Iban.compose(...), Modulo97 and CountryCodes all take
CharSequence, while toIban(), toIbanOrNull() and isValidIban() are extensions on String.
That split is deliberate. The extensions are the Kotlin sugar, and Kotlin's own conversion
extensions (toInt(), toBoolean()) are declared on String too; a String receiver also exports
as an NSString * parameter of the generated IbanKt facade, so IbanKt.toIban(_:) stays
type-checked from Swift, where a CharSequence receiver would erase to an untyped id. If you hold
something else — a StringBuilder, an Android Editable — Iban(input) and Iban.parse(input)
take it as-is; for the exception-free pair, convert first (builder.toString().toIbanOrNull()).
Migrating from java-iban, from kiban 0.3.0 and earlier, or from the Result-returning 0.4.0 API?
See MIGRATION.md.
Every example above is walked through by samples/jvm-cli, a runnable demo
of the API; see samples/ for that and a Swift consumer exercising the library
through Kotlin/Native's Objective-C interop — see
docs/9-swift-interop-review.md for the review that found the
0.4.0 Result-returning API didn't survive the trip to Swift, which is what this strict,
@Throws-annotated API is meant to fix.
I (Barend) like the Joda-Time library, and I try to follow the same design principles. I'm explicitly targetting Android, which at the time this library started was still on Java 1.6. I'm trying to keep the library as simple as I can.
Iban objects are immutable, and the Iban therein is non-empty and valid. There is no support for partial or invalid IBANs. Note that "valid" isn't as strict as it could be:
QA2!n4!a21!c) is not enforced. This seems to me like more work than necessary. The modulo-97 checksum catches most input errors anyway, and I don't want to force a memory-hungry regex check onto Android users. Speaking of Android, this mask could be used for keyboard switching on an Iban EditText, but that's for a different open-source project.Iban.parse() method. This, to me, would look too much like Joda-Time's pluggable Chronology system, which leads to PoLS violations (background: Why JSR-310 isn't Joda-Time).Iban class. Currently, that's the support for extracting Bank and Branch identifiers, which lives in the CountryCode class.Adopted design choices from the Java library, plus:
IbanParseException on invalid input, rather than returning a Result. The exception type extends IllegalArgumentException, and callers who want typed errors can catch it and inspect the failure instead of matching on messages. Every throwing entry point carries @Throws(IbanParseException::class), which is load-bearing for Kotlin/Native's Objective-C interop: an exception escaping an unannotated function aborts the process there, rather than surfacing as a catchable Swift error.Modulo97 keeps throwing: it is a low-level utility whose errors indicate a contract violation, not invalid user input.The embedded country data (CountryCodesData.kt) and the country test data table are generated from the SWIFT IBAN Registry TXT. The registry TXT is not redistributable and is never committed: it lives only in the gitignored scripts/input/.
# Download "IBAN Registry (TXT)" in a browser from https://www.swift.com/standards/data-standards/iban,
# or try the scripted download. Headless Chromium is blocked by Swift's bot detection from at least
# some networks (which is why CI runs headed under Xvfb), so reach for --headed when this times out:
kotlin scripts/fetch_registry.main.kts --out scripts/input/iban-registry.txt
kotlin scripts/generate_country_data.main.kts --registry scripts/input/iban-registry.txt --rev <revision>
# The generator emits unformatted KotlinPoet output; without this the diff is thousands of
# whitespace-only lines:
./gradlew :library:ktfmtFormatKmpCommonMain :library:ktfmtFormatKmpCommonTestThe registry's release number has to be supplied by hand: the download endpoint sends no filename and the registry page states no release, so neither script can detect it. Read it off the registry PDF and pass it as --rev.
The generator validates every entry before writing (mod-97 checksum, declared length and country prefix, and bank/branch identifier positions cross-checked against the registry's own identifier examples), so a malformed or truncated download fails loudly instead of landing in the library.
The registry TXT cannot be committed, so the generator's parser is covered by a synthetic fixture instead — invented countries in the registry's own format, carrying the quirks it has to survive. kotlin scripts/generate_country_data.main.kts --self-check runs the parser, the overlay merge and the validation over scripts/testdata/synthetic-registry.txt and writes nothing; the sync workflow runs it before every download.
Contributions are welcome — issues, discussion and pull requests alike. Help shape a modern, multiplatform IBAN utility library.
CONTRIBUTING.md has the practical part: the checks to run before pushing
(./gradlew jvmTest apiCheck ktfmtCheck), when to regenerate the API dumps, and what a pull request
is expected to carry. Found a security problem instead? Do not open an issue — SECURITY.md
has the private reporting channel.
VERSIONING.md is the policy: what a major, minor and patch bump each mean, what
the binary compatibility guarantee covers and how apiCheck enforces it against the committed API
dumps, the deprecation cycle that applies to post-1.0 removals, and the consumer requirements above.
Until 1.0 ships, the API is still open to breaking change; every break so far is mapped in
MIGRATION.md and listed in CHANGELOG.md.
This Kotlin Multiplatform library is a continuation and re-implementation of the original java-iban library by Barend Garvelink. It delivers IBAN validation, formatting, and country-specific IBAN details. The library is aimed to fulfill the same features as the original but in a Kotlin Multiplatform environment.
API stability. Kiban is pre-1.0 and the API is still being settled, so the 0.x line still breaks compatibility where the 1.0 freeze would otherwise lock in a mistake, and removals land without a deprecation cycle. What that becomes at 1.0 — the semver contract, the binary compatibility guarantee and the deprecation cycle for post-1.0 removals — is written down in VERSIONING.md.
The original java-iban library laid a solid foundation for IBAN validation and utility functions in Java environments. This library reimagines those capabilities with Kotlin's cross-platform features, making it ready for use on multiple platforms such as JVM, Android, iOS, and more.
Artifacts are published to Maven Central.
dependencies {
implementation("nl.bijdorpstudio.kiban:kiban:0.5.0")
}In a multiplatform project, add it to commonMain:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("nl.bijdorpstudio.kiban:kiban:0.5.0")
}
}
}Supported targets: JVM, Android, js (Node.js and browser), wasmJs (Node.js and browser), iOS,
macOS (Apple silicon only), watchOS, tvOS, linuxX64, linuxArm64, and mingwX64.
The js and wasmJs artifacts serve Kotlin/JS and Kotlin/Wasm consumers, not plain
JavaScript or TypeScript ones. Nothing in the library is annotated @JsExport, so none of its
declarations are reachable from hand-written JavaScript, and no TypeScript definitions are
generated for them; the artifacts are published to Maven Central as Kotlin klibs for a Gradle
build to resolve, not to npm. That is a deliberate scope boundary rather than an omission:
@JsExport accepts only a subset of Kotlin types, and once names, overloads and nullability are
visible to JavaScript they become a second frozen contract to maintain alongside the Kotlin one.
Exporting to plain JS stays possible as a purely additive change after 1.0 if there is demand for
it — withdrawing it again would not be.
| Requirement | |
|---|---|
| Kotlin |
2.4.0 or newer, for both the consumer's compiler and its apiVersion
|
| Java | bytecode level 17, so a JDK 17 or newer runtime |
| Android |
minSdk 24
|
| macOS |
Apple silicon only — the macOS artifact and the macOS slice of the Kiban XCFramework have been macosArm64 since 0.5.0 |
These are part of the compatibility contract rather than incidental facts about the current build:
raising any of them breaks consumers who cannot follow, so it only happens in a major release. The
Kotlin and Java floors are pinned by the tapmoc plugin, independently of the compiler the library
is built with, which moves forward on its own schedule. See VERSIONING.md.
The Kotlin floor is 2.4.0 so that an optional parameter can be added to a published function
without breaking binary compatibility: @IntroducedAt and ExperimentalVersionOverloading do not
resolve below a 2.4 languageVersion. It moved up from 2.3.0 before the 1.0 freeze deliberately,
since afterwards the same move would cost a major release. A second constraint keeps it from ever
going below 2.3: CountryCodes.lastUpdateDate returns kotlin.time.Instant, which the standard
library only makes non-experimental from 2.3, so a lower floor would ask callers for
@OptIn(kotlin.time.ExperimentalTime::class) to read a frozen public property. See
docs/144-instant-api-stability.md.
Parsing is strict: invalid input throws a typed IbanParseException, so you don't need to unwrap a
Result for the common case. The exception-free toIbanOrNull() and isValidIban() are there for
when you want to check input without paying for a stack trace.
Input must be ASCII. ISO 13616 defines the IBAN character set as A-Z0-9, so upper case ASCII
letters, ASCII digits and (ASCII 0x20) spaces are all that parse; non-ASCII look-alikes such as
fullwidth 9 or Arabic-Indic ٩ digits are rejected rather than normalized, because silently
rewriting a bank account identifier hides upstream data corruption. NFKC-normalize user input before
parsing if your input layer can produce them.
The whitespace leniency is just as narrow: the (ASCII 0x20) space is ignored between the first and
last character, so both "NL91ABNA0417164300" and "NL91 ABNA 0417 1643 00" parse, but a leading
or trailing space is rejected and so is any other whitespace anywhere — a tab or a non-breaking
space mid-IBAN is a paste artifact, not grouping, and is reported as an invalid character. Trim
before parsing if your input layer can produce them.
// The primary entry point. Throws IbanParseException on invalid input.
val iban: Iban = Iban( "NL91ABNA0417164300" )
// Or use the String extension; same throwing behaviour.
val parsed: Iban = "NL91ABNA0417164300".toIban()
// Exception-free fast paths.
val orNull: Iban? = "NL91ABNA0417164301".toIbanOrNull() // null, check digits are wrong
val isValid: Boolean = "NL91ABNA0417164300".isValidIban() // true
// Failures carry a typed reason, so you never have to match on messages.
try {
Iban( input )
} catch ( failure: IbanParseException ) {
when ( failure ) {
is IbanParseException.UnknownCountryCode -> reportUnknown( failure.countryCode )
is IbanParseException.WrongLength -> reportLength( failure.expectedLength, failure.actualLength )
is IbanParseException.WrongChecksum -> reportChecksum()
is IbanParseException.Malformed -> reportMalformed( failure.kind )
}
}
// toString() emits standard formatting, plain is compact.
val formatted = iban.toString() // "NL91 ABNA 0417 1643 00"
val plain = iban.plain // "NL91ABNA0417164300"
// Input may be formatted.
val anotherIban = Iban( "BE68 5390 0754 7034" )
// Iban implements Comparable<T>.
val ibans = getListOfIBANs()
ibans.sorted() // sorts in lexical order
// The equals() and hashCode() methods are implemented.
val ibansAsKeys = mutableMapOf<Iban, String>()
ibansAsKeys.put( iban, "this is fine" )
// You can use the Modulo97 class directly to compute or verify the check digits on an input.
val candidate = "GB29 NWBK 6016 1331 9268 19"
val valid = Modulo97.verifyCheckDigits( candidate ) // true
// Compose the IBAN for a country and BBAN; also throws on invalid input.
Iban.compose( "BI", "10000100010000332045181" ) // BI4210000100010000332045181
// You can query whether an IBAN is of a SEPA-participating country
val isSepa = Iban( candidate ).isSEPA // true
// You can query whether an IBAN is in the SWIFT Registry
val isRegistered = Iban( candidate ).isInSwiftRegistry // true
// Modulo97 API methods take CharSequence, not just String.
val builder = StringBuilder( "LU000019400644750000" )
val checkDigits = Modulo97.calculateCheckDigits( builder ) // 28
// Modulo97 API can calculate check digits, also for non-iban inputs.
// It does assume/require that the check digits are on indices 2 and 3.
Modulo97.calculateCheckDigits( "GB", "NWBK60161331926819" ) // 29
Modulo97.calculateCheckDigits( "XX", "X" ) // 72
// Get the expected IBAN length for a country code:
val expectedLength: Int? = CountryCodes.ibanLength( "DK" ) // 18
// Get the Bank Identifier and Branch Identifier:
val bankId: String? = iban.bankIdentifier
val branchId: String? = iban.branchIdentifier
// Get the BBAN, the counterpart of what Iban.compose() takes:
val bban: String = iban.bban // round trips: Iban.compose( iban.countryCode, iban.bban )Modulo97 is the one part of the library that still throws unconditionally: its inputs are
programmer-supplied, so a bad one is a contract violation rather than user input to be validated.
Iban's throwing entry points (Iban(...), Iban.compose(...), String.toIban()) are annotated
@Throws(IbanParseException::class), so Kotlin/Native's Objective-C exporter emits an NSError**
out-parameter and Swift sees a normal throws function instead of the process aborting on an
unannotated exception.
Iban(input) is operator fun invoke on the companion object, which only Kotlin has call syntax
for: Java reads it as Iban.Companion.invoke(...) and Swift as Iban.companion.invoke(input:).
For those callers there is Iban.parse(input), a named alias that parses identically, and
Iban.compose(...) is @JvmStatic for the same reason. Kotlin callers should keep using
Iban(input).
Iban(...), Iban.parse(...), Iban.compose(...), Modulo97 and CountryCodes all take
CharSequence, while toIban(), toIbanOrNull() and isValidIban() are extensions on String.
That split is deliberate. The extensions are the Kotlin sugar, and Kotlin's own conversion
extensions (toInt(), toBoolean()) are declared on String too; a String receiver also exports
as an NSString * parameter of the generated IbanKt facade, so IbanKt.toIban(_:) stays
type-checked from Swift, where a CharSequence receiver would erase to an untyped id. If you hold
something else — a StringBuilder, an Android Editable — Iban(input) and Iban.parse(input)
take it as-is; for the exception-free pair, convert first (builder.toString().toIbanOrNull()).
Migrating from java-iban, from kiban 0.3.0 and earlier, or from the Result-returning 0.4.0 API?
See MIGRATION.md.
Every example above is walked through by samples/jvm-cli, a runnable demo
of the API; see samples/ for that and a Swift consumer exercising the library
through Kotlin/Native's Objective-C interop — see
docs/9-swift-interop-review.md for the review that found the
0.4.0 Result-returning API didn't survive the trip to Swift, which is what this strict,
@Throws-annotated API is meant to fix.
I (Barend) like the Joda-Time library, and I try to follow the same design principles. I'm explicitly targetting Android, which at the time this library started was still on Java 1.6. I'm trying to keep the library as simple as I can.
Iban objects are immutable, and the Iban therein is non-empty and valid. There is no support for partial or invalid IBANs. Note that "valid" isn't as strict as it could be:
QA2!n4!a21!c) is not enforced. This seems to me like more work than necessary. The modulo-97 checksum catches most input errors anyway, and I don't want to force a memory-hungry regex check onto Android users. Speaking of Android, this mask could be used for keyboard switching on an Iban EditText, but that's for a different open-source project.Iban.parse() method. This, to me, would look too much like Joda-Time's pluggable Chronology system, which leads to PoLS violations (background: Why JSR-310 isn't Joda-Time).Iban class. Currently, that's the support for extracting Bank and Branch identifiers, which lives in the CountryCode class.Adopted design choices from the Java library, plus:
IbanParseException on invalid input, rather than returning a Result. The exception type extends IllegalArgumentException, and callers who want typed errors can catch it and inspect the failure instead of matching on messages. Every throwing entry point carries @Throws(IbanParseException::class), which is load-bearing for Kotlin/Native's Objective-C interop: an exception escaping an unannotated function aborts the process there, rather than surfacing as a catchable Swift error.Modulo97 keeps throwing: it is a low-level utility whose errors indicate a contract violation, not invalid user input.The embedded country data (CountryCodesData.kt) and the country test data table are generated from the SWIFT IBAN Registry TXT. The registry TXT is not redistributable and is never committed: it lives only in the gitignored scripts/input/.
# Download "IBAN Registry (TXT)" in a browser from https://www.swift.com/standards/data-standards/iban,
# or try the scripted download. Headless Chromium is blocked by Swift's bot detection from at least
# some networks (which is why CI runs headed under Xvfb), so reach for --headed when this times out:
kotlin scripts/fetch_registry.main.kts --out scripts/input/iban-registry.txt
kotlin scripts/generate_country_data.main.kts --registry scripts/input/iban-registry.txt --rev <revision>
# The generator emits unformatted KotlinPoet output; without this the diff is thousands of
# whitespace-only lines:
./gradlew :library:ktfmtFormatKmpCommonMain :library:ktfmtFormatKmpCommonTestThe registry's release number has to be supplied by hand: the download endpoint sends no filename and the registry page states no release, so neither script can detect it. Read it off the registry PDF and pass it as --rev.
The generator validates every entry before writing (mod-97 checksum, declared length and country prefix, and bank/branch identifier positions cross-checked against the registry's own identifier examples), so a malformed or truncated download fails loudly instead of landing in the library.
The registry TXT cannot be committed, so the generator's parser is covered by a synthetic fixture instead — invented countries in the registry's own format, carrying the quirks it has to survive. kotlin scripts/generate_country_data.main.kts --self-check runs the parser, the overlay merge and the validation over scripts/testdata/synthetic-registry.txt and writes nothing; the sync workflow runs it before every download.
Contributions are welcome — issues, discussion and pull requests alike. Help shape a modern, multiplatform IBAN utility library.
CONTRIBUTING.md has the practical part: the checks to run before pushing
(./gradlew jvmTest apiCheck ktfmtCheck), when to regenerate the API dumps, and what a pull request
is expected to carry. Found a security problem instead? Do not open an issue — SECURITY.md
has the private reporting channel.
VERSIONING.md is the policy: what a major, minor and patch bump each mean, what
the binary compatibility guarantee covers and how apiCheck enforces it against the committed API
dumps, the deprecation cycle that applies to post-1.0 removals, and the consumer requirements above.
Until 1.0 ships, the API is still open to breaking change; every break so far is mapped in
MIGRATION.md and listed in CHANGELOG.md.