A Gradle plugin that detects Kotlin metadata version drift in your dependencies — the kind of mismatch that compiles fine but can cause runtime incompatibilities.
Every Kotlin class compiled to JVM bytecode carries a @kotlin.Metadata annotation with a version field (mv). When a dependency was compiled with a significantly older Kotlin version than your project uses, this drift is a compatibility risk that often escapes compile-time detection. The result can be NoSuchMethodError, IncompatibleClassChangeError, or other failures that only surface at runtime.
This is especially common when:
- You upgrade to Kotlin 2.x but a transitive dependency was compiled with Kotlin 1.5
- A vendor SDK bundles an old version of Ktor or another Kotlin library inside a fat JAR
- An old library hasn't been updated in years and you pull it in through a transitive chain
- You use Kotlin Multiplatform and dependencies come from different Kotlin versions
What kdrift does:
- Detects Kotlin metadata version drift in dependency artifacts
- Drift is a strong risk signal, especially in shaded or vendor-controlled artifacts
- Findings should be treated as investigation signals and compatibility risks
What kdrift does not prove:
- Not every finding will cause a runtime failure — metadata version is a proxy for compiler version, not an exact match
- False positives are possible (e.g., stable internal APIs that work fine across Kotlin versions)
- kdrift does not analyze bytecode compatibility — it detects metadata version deltas
- Package attribution uses top-level packages (first two path segments) which can be imprecise
Why existing tools miss this:
- Gradle dependency resolution sees Maven coordinates, not classes shaded inside vendor artifacts
- Version resolution strategies cannot upgrade classes baked into fat/shaded JARs
- Standard dependency tools do not recursively inspect Kotlin metadata inside nested archives
After upgrading to Kotlin 2.2.0, a vendor SDK starts failing with a cryptic error code. No stack trace, no useful diagnostic. After days of investigation and decompiling the SDK, you discover the root cause: the SDK internally bundles a popular Kotlin networking library compiled against Kotlin 1.8.x, carrying metadata that represents a compatibility risk against the Kotlin 2.2.0 project.
kdrift surfaces this at build time. It scans inside fat/shaded JARs, finds the bundled classes, and tells you exactly which internal package drifted:
kdrift: [WARN] vendor-sdk-1.0.jar — compiled with Kotlin 1.8.0, project: 2.2.0
kdrift: ↳ io.ktor: Kotlin 1.8.0
No decompilation needed. No days of debugging. kdrift surfaces the drift the moment you bump your Kotlin version, giving you an investigation starting point instead of a mystery.
// build.gradle.kts
plugins {
id("io.github.iamjosephmj.kdrift") version "0.2.0"
}// build.gradle
plugins {
id 'io.github.iamjosephmj.kdrift' version '0.2.0'
}./gradlew kdriftCheckWhen the Kotlin JVM or Kotlin Android plugin is applied, kdriftCheck automatically runs as part of ./gradlew check. No extra wiring needed.
kdrift: [NEW] [WARN] vendor-sdk-1.0.jar — compiled with Kotlin 1.8.0, project: 2.2.0
kdrift: ↳ io.ktor: Kotlin 1.8.0 (23 classes)
kdrift: 💡 Ktor is a Kotlin-first framework tightly coupled to the Kotlin compiler version.
kdrift: 💡 Check for a newer version of the dependency.
kdrift: [NEW] [ERROR] legacy-utils-0.9.jar — compiled with Kotlin 1.3.72, project: 2.2.0
kdrift: Full report at /path/to/build/reports/kdrift/report.txt
kdrift: JSON report at /path/to/build/reports/kdrift/report.json
Two report files are generated in build/reports/kdrift/:
report.txt— human-readable text reportreport.json— machine-readable JSON for CI (see CI Integration)
Text report example:
=== kdrift Report ===
Project Kotlin version: 2.2.0
Scan date: 2026-03-07
Found 2 dependencies with potential incompatibilities:
[ERROR] legacy-utils-0.9.jar
Compiled with: Kotlin 1.3.72
Project: Kotlin 2.2.0
Classes: 47
Hints:
- Check for a newer version of the dependency.
[WARN] vendor-sdk-1.0.jar
Compiled with: Kotlin 1.8.0
Project: Kotlin 2.2.0
Classes: 150
Package breakdown:
- io.ktor: Kotlin 1.8.0 (23 classes)
Samples: io.ktor.client.HttpClient, io.ktor.client.engine.OkHttpEngine
- com.vendor: Kotlin 2.0.0 (127 classes)
Samples: com.vendor.sdk.Auth, com.vendor.sdk.Config
Hints:
- This artifact contains packages compiled with different Kotlin versions, indicating a fat/shaded JAR with bundled dependencies.
- Ktor is a Kotlin-first framework tightly coupled to the Kotlin compiler version.
- Check for a newer version of the dependency.
The package breakdown appears when a single artifact contains classes from different packages compiled with different Kotlin versions — the telltale sign of a fat/shaded JAR bundling an old dependency. It tells you exactly which internal library is the problem, including class counts and sample class names per package.
When a baseline is active, findings are labeled [NEW], [WORSENED], or [BASELINE] to distinguish new issues from accepted ones.
kdrift performs a deep recursive scan of every dependency artifact:
| Structure | Scanned? |
|---|---|
JAR → .class files |
Yes |
JAR → nested JAR → .class files |
Yes (recursive) |
| JAR → nested JAR → nested JAR → ... | Yes (any depth) |
AAR → embedded JAR → .class files |
Yes |
AAR → embedded JAR → nested JAR → .class files |
Yes |
This means kdrift catches version drift in:
- Regular dependencies (direct and transitive)
- Fat/shaded JARs that bundle dependencies inline (classes repackaged into the JAR)
- Nested JARs inside fat JARs (e.g., Spring Boot executable JARs with
BOOT-INF/lib/*.jar) - Vendor SDKs that bundle old Kotlin libraries internally
- Android AARs with embedded JARs
kdrift {
// Policy mode controls when the build fails.
// ADVISORY (default): never fails, only reports
// FAIL_ON_ERROR: fails on ERROR severity
// FAIL_ON_WARNING: fails on WARNING or ERROR
policyMode.set("FAIL_ON_ERROR")
// Legacy option — equivalent to policyMode = FAIL_ON_ERROR.
// Use policyMode instead for new projects.
failOnIncompatible.set(true)
// Major version gap to start flagging.
// Default: 1 (flags when dependency is 1.x and project is 2.x)
// Gap == threshold -> WARNING
// Gap > threshold -> ERROR
majorVersionGapThreshold.set(1)
// Minor version gap to flag within the same major version.
// Default: not set (disabled)
// Useful for catching 1.5 vs 1.9 drift where metadata formats changed.
minorVersionGapThreshold.set(3)
// Dependencies to skip. Use exact artifact names or group:artifact coordinates.
ignoredDependencies.set(listOf(
"io.ktor:ktor-client-core",
"legacy-internal-sdk-1.0"
))
// Baseline file. When present, kdriftCheck only fails on new or worsened findings.
// Generate with: ./gradlew kdriftBaseline
baselineFile.set(file("kdrift-baseline.json"))
// Suppressions file. Suppressed artifacts are logged but do not cause failures.
suppressionsFile.set(file("kdrift-suppressions.json"))
// Override which Gradle configurations to scan.
// Default: auto-detects (releaseRuntimeClasspath, runtimeClasspath, or debugRuntimeClasspath)
configurations.set(listOf("runtimeClasspath"))
// Override the project Kotlin version if auto-detection doesn't work.
// Normally detected from the Kotlin plugin automatically.
projectKotlinVersion.set("2.2.0")
}| Property | Type | Default | Description |
|---|---|---|---|
policyMode |
String |
"ADVISORY" |
When to fail: ADVISORY, FAIL_ON_ERROR, or FAIL_ON_WARNING |
failOnIncompatible |
Boolean |
false |
Legacy: maps to FAIL_ON_ERROR when true |
majorVersionGapThreshold |
Int |
1 |
Major version gap that triggers warnings/errors |
minorVersionGapThreshold |
Int |
not set | Minor version gap threshold (disabled by default) |
ignoredDependencies |
List<String> |
[] |
Artifact names or group:artifact coordinates to skip |
baselineFile |
File |
not set | Path to baseline JSON (enables incremental adoption) |
suppressionsFile |
File |
not set | Path to suppressions JSON (skip specific artifacts with reasons) |
configurations |
List<String> |
auto-detect | Gradle configurations to scan |
projectKotlinVersion |
String |
auto-detect | Your project's Kotlin version |
Baselines let teams adopt kdrift gradually without blocking builds on existing drift.
# Generate a baseline from current findings
./gradlew kdriftBaselineThis creates kdrift-baseline.json in your project root. Once a baseline exists and is configured via baselineFile, kdriftCheck will:
- Label existing findings as
[BASELINE]and skip them for failure decisions - Fail only on
[NEW]findings (artifacts not in the baseline) - Fail on
[WORSENED]findings (severity increased since the baseline was captured)
For artifacts where drift is known and accepted, use a suppressions file:
{
"suppressions": [
{
"artifact": "vendor-sdk-1.0.jar",
"reason": "Vendor confirmed no runtime impact; awaiting SDK update Q3 2026"
}
]
}Suppressed artifacts are skipped during scanning but logged at info level with their reason. Run with --info to see suppression messages.
Every run generates a machine-readable JSON report at build/reports/kdrift/report.json. No extra configuration needed.
{
"projectKotlinVersion": "2.2.0",
"scanDate": "2026-03-07",
"totalDependenciesScanned": 48,
"incompatibleCount": 2,
"errorCount": 1,
"warningCount": 1,
"dependencies": [
{
"artifact": "vendor-sdk-1.0.jar",
"compiledWithKotlin": "1.8.0",
"severity": "WARNING",
"classCount": 150,
"packageBreakdown": {
"io.ktor": "1.8.0",
"com.vendor": "2.0.0"
},
"sampleClasses": {
"io.ktor": ["io.ktor.client.HttpClient", "io.ktor.client.engine.OkHttpEngine"],
"com.vendor": ["com.vendor.sdk.Auth", "com.vendor.sdk.Config"]
},
"packageClassCounts": {
"io.ktor": 23,
"com.vendor": 127
},
"nestedPaths": ["vendor-sdk-1.0.jar!/libs/ktor-client.jar"],
"oldestClassVersion": "1.8.0",
"newestClassVersion": "2.0.0",
"hints": [
"Classes found inside nested archives — this dependency bundles other libraries internally.",
"Ktor is a Kotlin-first framework tightly coupled to the Kotlin compiler version.",
"Check for a newer version of the dependency."
]
}
]
}Key JSON fields:
packageBreakdown— top-level packages mapped to their oldest Kotlin metadata version, pinpointing which bundled library driftedsampleClasses— up to 3 representative class names per package for concrete evidencepackageClassCounts— number of Kotlin classes per packagenestedPaths— archive paths where drifted classes were found (e.g.,sdk.jar!/libs/inner.jar)oldestClassVersion/newestClassVersion— version range across all packageshints— dynamically generated remediation hints based on structural analysis and known package families
You can consume this in CI to post PR comments, feed dashboards, or gate merges:
# GitHub Actions example — fail if any errors
./gradlew kdriftCheck
ERRORS=$(jq '.errorCount' build/reports/kdrift/report.json)
if [ "$ERRORS" -gt 0 ]; then
echo "::error::kdrift found $ERRORS dependencies with incompatible Kotlin metadata"
jq -r '.dependencies[] | select(.severity == "ERROR") | "::error::\(.artifact) compiled with Kotlin \(.compiledWithKotlin)"' build/reports/kdrift/report.json
exit 1
fiFor programmatic access directly in your build script, use the onResult callback. The receiver is a KDriftResult — use this to access properties (Gradle Action receiver style):
kdrift {
onResult {
// 'this' is a KDriftResult
// Write JSON to a custom location
file("build/kdrift-ci.json").writeText(toJson())
// Or use the structured data directly
if (errorCount > 0) {
dependencies.filter { it.severity == "ERROR" }.forEach {
println("BLOCKED: ${it.artifact} uses Kotlin ${it.compiledWithKotlin}")
it.packageBreakdown.forEach { (pkg, ver) ->
println(" ↳ $pkg: $ver")
}
}
}
}
}The KDriftResult object contains:
| Field | Type | Description |
|---|---|---|
projectKotlinVersion |
String |
Your project's Kotlin version |
scanDate |
String |
ISO date of the scan |
totalDependenciesScanned |
Int |
Total Kotlin dependencies scanned |
incompatibleCount |
Int |
Number of flagged dependencies |
errorCount |
Int |
Number of ERROR-level issues |
warningCount |
Int |
Number of WARNING-level issues |
dependencies |
List |
See dependency entry fields below |
Each dependency entry contains:
| Field | Type | Description |
|---|---|---|
artifact |
String |
Artifact file name |
compiledWithKotlin |
String |
Oldest Kotlin metadata version found |
severity |
String |
"WARNING" or "ERROR" |
classCount |
Int |
Number of Kotlin classes in the artifact |
packageBreakdown |
Map<String, String> |
Top-level packages with their oldest version |
sampleClasses |
Map<String, List<String>> |
Up to 3 representative class names per package |
packageClassCounts |
Map<String, Int> |
Number of classes per package |
nestedPaths |
List<String> |
Archive paths where drifted classes were found |
oldestClassVersion |
String? |
Oldest version across all packages |
newestClassVersion |
String? |
Newest version across all packages |
hints |
List<String> |
Remediation hints for this finding |
Call toJson() on the result to get the JSON string shown above.
- Resolves the specified (or auto-detected) Gradle configuration to get all dependency JARs and AARs
- Skips suppressed artifacts (if a suppressions file is configured)
- For each JAR, reads every
.classfile using ASM and looks for the@kotlin.Metadataannotation - Recursively scans nested JARs inside fat/shaded JARs (any depth), tracking the full archive path (e.g.,
sdk.jar!/libs/inner.jar!/com/example/Foo.class) - For AARs, extracts embedded JARs and scans them the same way
- Extracts the
mv(metadata version) array — this tells you which Kotlin version compiled that class - Groups classes by top-level package, collecting the oldest version, class count, and up to 3 sample class names per package
- Compares the oldest metadata version found in each dependency against your project's Kotlin version
- Reports WARNING or ERROR based on how far the versions have drifted
- Generates remediation hints dynamically based on structural signals (nesting, mixed versions, version gap) and known package families (Ktor, Compose, coroutines, stdlib, etc.)
- If a baseline exists, classifies findings as
[NEW],[WORSENED], or[BASELINE]— only new or worsened findings trigger build failures
| Level | Meaning | Example (threshold=1) |
|---|---|---|
| OK | Same major version, within tolerance | Project 2.2, dep 2.0 |
| WARNING | Gap equals the threshold | Project 2.0, dep 1.9 |
| ERROR | Gap exceeds the threshold | Project 3.0, dep 1.5 |
When minorVersionGapThreshold is set, the same WARNING/ERROR logic applies to minor versions within the same major.
- Gradle 7.0+
- Works with Kotlin JVM and Kotlin Android projects
- No runtime dependencies added to your project — kdrift only runs at build time
Apache License 2.0