Concepts¶
The ideas behind every serialkompat report: the baseline, backward versus forward
compatibility, BREAK versus WARN, and a glossary of the terms used across these docs.
The baseline¶
The baseline is the schema you compare against. serialkompat doesn't read it from a checked-in file. It rebuilds it on every run from a git ref:
flowchart LR
REF["baselineRef<br/>(your target branch)"] --> WT["Temporary git worktree"]
WT --> OLD["Extract old schema"]
CUR["Your PR<br/>(compiled classes)"] --> NEW["Extract new schema"]
OLD --> DIFF["Diff + classify"]
NEW --> DIFF
DIFF --> REPORT["Report<br/>BREAK / WARN"]
Because the old schema comes from the real code on that ref, it can't go stale. The
flip side is that baselineRef must already contain the plugin; see
Quick start → Your first PR. If you don't set
baselineRef, serialkompat uses your repository's default branch. The mechanics are in
Baseline & caching.
Backward vs forward¶
Compatibility has two directions, and they fail differently. During a deploy, old and new versions of your code run side by side, and each can read what the other wrote:
flowchart LR
subgraph OLD["Old release (v1)"]
OW["Writer v1"]
OR["Reader v1"]
end
subgraph NEW["New release (v2)"]
NW["Writer v2"]
NR["Reader v2"]
end
OW -- "backward: new code reads old data" --> NR
NW -- "forward: old code reads new data" --> OR
- Backward (new code reads old data): the new reader must decode payloads the old writer already produced, such as messages in a queue or rows in a database.
- Forward (old code reads new data): the old reader, still running during a rolling deploy or on a client that hasn't updated, must decode what the new writer produces.
Every finding says which direction broke, for example (backward).
Which direction do I pick?¶
direction tells the classifier which reader/writer pairing must keep working after
the change. Pick it from how the schema is actually used:
| Direction | Guarantees | Use when |
|---|---|---|
BACKWARD |
New code can read data written by old code | Rolling deploys: a newer service version must decode messages/events produced by instances still running the old version |
FORWARD |
Old code can read data written by new code | Persisted data with slow migrations, or mixed-version consumers: an older reader (a replica, a cached job, a client that hasn't upgraded yet) must decode records a newer writer just produced |
FULL |
Both | Public APIs, shared wire formats, or persisted data with no controlled rollout order. The safest default when you don't control both ends |
FULL is the default. Keep it unless you know only one direction matters.
You need FULL for a queue whose producers and consumers deploy independently, or for
long-lived stored rows that code from any past version may read. A rolling deploy of a
single service, where old instances drain within minutes, only needs BACKWARD for that
window.
Warning
BACKWARD and FORWARD each drop half the guarantee. Choose one deliberately,
never as a way to silence findings.
The setting is direction in the serialkompat { } block,
or --direction on the CLI.
BREAK vs WARN¶
Every finding has a severity.
BREAK
Decoding fails: the reader throws, in the stated direction. A
BREAK fails the build (unless you set failOnBreaking to false). You have four
ways out: fix it, accept it, declare a rename, or bump the major version. See
Got a BREAK?.
WARN
Either the outcome depends on your configuration, or decoding succeeds but the data
is silently wrong, defaulted or dropped. A WARN never fails the build. Read it, and decide whether your setup is affected. A type serialkompat couldn't analyse is also a WARN, because
unanalysable never means safe; see Limitations.
Breaks you have sanctioned with acceptedBreaks are called accepted breaks in these
docs, after the DSL name. The console report labels them "acknowledged" and SARIF labels
them "accepted"; they are the same thing, and they no longer fail the build.
The full list of rules, and which severity each gives under which configuration, is in Rules.
Glossary¶
| Term | Meaning |
|---|---|
| Contract | The wire shape serialkompat extracts for one serial name: its properties, optionality, defaults, nullability, enum values or sealed subtypes. Your model is a set of contracts |
| Serial name | The name a type goes by on the wire: @SerialName if present, otherwise the fully-qualified class name. A sealed or polymorphic subtype is reported as Base/sub |
| Root type | A @Serializable type you list in types (or that discovery finds). Everything reachable from a root, such as nested types and sealed subtypes, is checked too |
| Baseline | The old schema you compare against, extracted live from baselineRef. See The baseline |
| Snapshot | The text file the extractor writes: every contract plus the Json or Cbor config. serialkompatExtract writes build/serialkompat/current.snapshot, and the CLI diffs two of them |
| Finding | One detected change: a severity, a rule id, the type it is about, a direction, and a suggested fix. A report is a list of findings |
| OPAQUE / coverage gap | A type serialkompat couldn't fully analyse is recorded as an OPAQUE contract and reported as a COVERAGE_GAP WARN, never a silent pass. See Limitations |
| Pairwise vs history check | A pairwise check compares your code against one baseline (serialkompatCheck). A history check compares it against every recorded release (serialkompatCheckHistory). See Persisted data & history |
| Accepted break | A break you sanctioned in acceptedBreaks. The console report calls it "acknowledged"; SARIF calls it "accepted" |