Setup
You can run serialkompat three ways: as a Gradle plugin, as a standalone CLI, or as a GitHub Action. Most projects start with the Gradle plugin.
Gradle plugin
The plugin is on the
Gradle Plugin Portal,
so gradlePluginPortal() (Gradle's default plugin repository) is all you need. Apply
it by id and version, and configure it. (0.1.0 is on Maven Central only; to use it, add
mavenCentral() to pluginManagement.repositories in settings.gradle.kts.)
import com.chrisjenx.serialkompat.core.CompatibilityDirection
plugins {
kotlin("jvm")
kotlin("plugin.serialization")
id("com.chrisjenx.serialkompat") version "0.2.0"
}
serialkompat {
types.set(listOf("com.example.wire.OrderEvent", "com.example.wire.Payment"))
jsonInstance.set("com.example.wire.WireJson.instance")
// baselineRef is optional. Unset, it auto-detects your default branch.
direction.set(CompatibilityDirection.FULL)
failOnBreaking.set(true)
failOnEmptyBaseline.set(true)
}
The baseline is the schema you compare against. serialkompat doesn't read it
from a checked-in file. It checks out baselineRef in a temporary git worktree and
runs serialkompatExtract there, so the baseline can't go stale. The plugin must
therefore be applied on baselineRef too; see
Quick start → Your first run.
Extraction runs on your project's own runtime classpath, so your models are read with the same kotlinx-serialization version they were compiled against.
serialkompat { } reference
The most common options. Configuration
has the full list, including the history { } and reports { } blocks.
| Property | Type | Default | Purpose |
|---|---|---|---|
types |
ListProperty<String> |
[] (required under EXPLICIT discovery) |
FQNs of @Serializable root types to check |
discovery |
Property<DiscoveryMode> |
EXPLICIT |
How types are found when types is empty: EXPLICIT, OPT_OUT, or OPT_IN. See Discovery modes |
jsonInstance |
Property<String> |
unset | FQN of a Json instance describing the wire; unset = default Json. If set but it can't be loaded, extraction fails |
baselineRef |
Property<String> |
auto-detected | Git ref to check against. Unset ⇒ auto-detect the default branch (origin/HEAD → origin/main → origin/master → local main/master) |
direction |
Property<CompatibilityDirection> |
FULL |
BACKWARD, FORWARD, or FULL |
failOnBreaking |
Property<Boolean> |
true |
A BREAK finding fails the build |
failOnEmptyBaseline |
Property<Boolean> |
true |
An empty baseline fails the build, so removals can't be masked; set false for first adoption |
include |
ListProperty<String> |
[""] |
Serial-name prefixes in scope ("" = all) |
exclude |
ListProperty<String> |
[] |
Serial-name prefixes excluded |
acceptedBreaks |
ListProperty<String> |
[] |
Sanctioned breaks, "<serialName> <RULE> [DIRECTION]". A subtype is Base/sub or bare sub |
renames |
MapProperty<String,String> |
{} |
Declared serial-name moves old→new (avoids a remove + add) |
If neither types nor a non-EXPLICIT discovery mode is set, every serialkompat
task is skipped, so applying the plugin never breaks check on its own.
Tasks
| Task | Does |
|---|---|
serialkompatExtract |
Extracts the current schema to build/serialkompat/current.snapshot |
serialkompatCheck |
Extracts and diffs against baselineRef; wired into check |
serialkompatCheckAgainst |
Same as serialkompatCheck, but the ref can be overridden with -Pserialkompat.ref=<ref> |
serialkompatRecord |
Records the current schema into the published history (-Pserialkompat.recordVersion=X.Y.Z). See Recipes |
serialkompatCheckHistory |
Checks the current schema against every recorded version; wired into check (skipped until a version is recorded) |
The plugin supports the configuration cache, --parallel, and Isolated Projects.
serialkompatExtract is build-cacheable and relocatable. The check tasks are never
cached, on purpose. See
CI setup → Build cache & configuration cache.
See Quick start for running the check and reading a report, and
Configuration for direction, acceptedBreaks, and renames in depth.
CLI
The CLI diffs two snapshot files without a Gradle build in the loop. Use it for non-Gradle or cross-repo checks, such as comparing a producer and a consumer that live in different repositories.
The CLI isn't published as a download yet. Build it from a checkout of this repository:
$ ./gradlew :serialkompat-cli:installDist
$ serialkompat-cli/build/install/serialkompat-cli/bin/serialkompat-cli --help
The examples on this page call that launcher serialkompat; alias or rename it
as you like.
serialkompat diff <baseline.snapshot> <current.snapshot> [--direction=FULL|BACKWARD|FORWARD] [--format=console|json|sarif|github] [--no-fail] [--allow-empty-baseline]
The first file is the old schema and the second is the new one.
| Flag | Effect |
|---|---|
--direction=FULL\|BACKWARD\|FORWARD |
Compatibility direction to enforce (default FULL) |
--format=console\|json\|sarif\|github |
Output format for the report (default console). See Report formats |
--no-fail |
Exit 0 even if the diff finds breaking changes (the report still prints) |
--allow-empty-baseline |
Accept a baseline with no contracts. Use it only for first-time adoption |
--help, -h |
Print usage and exit 0 |
For example, serialkompat diff a.snapshot b.snapshot --format=sarif > report.sarif
writes a SARIF 2.1.0 log. --format only changes what is printed, never the exit code.
The CLI never crashes on bad input. A missing file, an unknown flag, or an invalid
--direction prints error: <message> and the usage line, then exits 2.
The CLI also fails closed on an empty baseline. If the baseline file has no
contracts but the current one has some, the diff would read as "everything was
added" and hide every removal. So the CLI prints an error: line, renders no
report, and exits 2. --no-fail doesn't skip this check. Pass
--allow-empty-baseline when an empty baseline is expected, such as on first-time
adoption. Two empty snapshots still pass.
| Exit code | Meaning |
|---|---|
0 |
No breaking findings |
1 |
At least one active BREAK finding (unless --no-fail) |
2 |
Usage error, unreadable snapshot, or an empty baseline (unless --allow-empty-baseline) |
Producing snapshots
The CLI only diffs. Extraction needs a compiled classpath, so produce each
.snapshot file with the Gradle plugin's serialkompatExtract task: once at your
baseline, and once at the current commit.
$ ./gradlew serialkompatExtract # writes build/serialkompat/current.snapshot
GitHub Action
The action runs serialkompatCheckAgainst (or a task you choose) and posts a sticky
PR comment with the summary and findings. On pull requests it also adds error
and warning annotations to the workflow run.
name: serialkompat
on: [pull_request, push]
jobs:
serialkompat:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/checkout@v7
with: { fetch-depth: 0 }
- uses: actions/setup-java@v6
with: { distribution: temurin, java-version: "17" }
- uses: chrisjenx/serialkompat@v0
with:
ref: origin/main
Requires serialkompat 0.1.0 or later. @v0 is a floating tag that tracks the
latest stable 0.x release.
The caller workflow needs two settings:
permissions: pull-requests: write. Without it, posting the sticky comment gets a 403 and the step fails for a reason unrelated to compatibility.fetch-depth: 0onactions/checkout. The baseline is extracted from a real git ref in a temporary worktree, which needs full history, not a shallow clone.
Inputs
| Input | Default | Purpose |
|---|---|---|
ref |
"" (empty) |
Baseline git ref to check against, passed as -Pserialkompat.ref=. Empty = use the plugin's baselineRef (or the auto-detected default branch) |
task |
serialkompatCheckAgainst |
Gradle task to run |
report-path |
build/serialkompat/report.json |
Path to the JSON report the sticky comment is built from |
gradle-args |
"" (empty) |
Extra arguments passed to Gradle |
The action declares no outputs. The job fails when the Gradle task exits
non-zero. On pull requests, the sticky comment
(marked <!-- serialkompat --> and updated in place on every push) follows the
check's exit code. It shows ❌ when the check failed, even with no BREAK (for
example an empty baseline). It shows ⚠️ when the check passed with findings,
including BREAKs when failOnBreaking is false. It shows ✅ when the check
passed clean. See CI setup → The sticky comment.
See CI setup for wiring this into a larger pipeline or other CI systems such as GitLab.