Adopting gradually¶
Bring serialkompat into an existing codebase without listing every wire type up front, using
discovery modes and two annotations. It is for teams that don't yet know which @Serializable
types are wire contracts. On your very first PR, start at Your first PR.
Discovery modes¶
discovery only matters when types is empty. It decides which of the discovered
@Serializable types get checked:
| Mode | Checked types | Use when |
|---|---|---|
EXPLICIT (default) |
Only types |
You maintain an explicit list of root types |
OPT_OUT |
Everything discovered, minus types annotated @SerialkompatIgnore |
Most types are wire contracts; a few (internal-only, unstable) opt out |
OPT_IN |
Only types annotated @SerialkompatChecked |
Gradual adoption. Nothing is checked until you annotate it |
Discovery scans your module's compiled classes for class-level @Serializable. The
annotations live in a small multiplatform artifact:
dependencies {
implementation("com.chrisjenx:serialkompat-annotations:0.3.1")
}
Put com.chrisjenx.serialkompat.annotations.SerialkompatIgnore or
com.chrisjenx.serialkompat.annotations.SerialkompatChecked on the @Serializable class
itself.
Precedence. These rules apply in this order, in every mode:
- A non-empty
typeslist always wins.discoveryis only consulted whentypesis empty. - Annotations filter the scanned set only. Types listed in a classpath manifest
(
META-INF/serialkompat/serializable-types.txt) skip annotation filtering and are always included inOPT_OUTandOPT_IN. includeandexcludeprefixes apply after discovery, in all modes. See Monorepos.
Kotlin Multiplatform: a KMP module works with discovery and extraction as long as it
declares a jvm() target. Extraction reads compiled JVM serializers, so a JVM target is
required. Annotate your models in commonMain. serialkompat-annotations is itself
multiplatform, so the annotations are available there.
Start with nothing checked¶
Switch to OPT_IN with no types list:
serialkompat {
discovery.set(com.chrisjenx.serialkompat.extractor.DiscoveryMode.OPT_IN)
}
Nothing is checked yet. As you review each wire type, annotate it:
@Serializable
@SerialkompatChecked
data class OrderEvent(
val id: String,
)
Each newly annotated type joins the gate on its next run. You don't touch the plugin config
or maintain a types list by hand.
The first annotated type
The PR that annotates your first type hits the empty-baseline guard from
Your first PR. The baseline on main still has zero
types, so the change looks like "everything was just added". Use the same fix: set
failOnEmptyBaseline.set(false) for that one PR, then remove it once baselineRef
includes it.
Flip to opt-out¶
When coverage is essentially complete, switch to DiscoveryMode.OPT_OUT so every discovered
type is checked, and mark the few intentionally unstable types with @SerialkompatIgnore:
serialkompat {
discovery.set(com.chrisjenx.serialkompat.extractor.DiscoveryMode.OPT_OUT)
}
@Serializable
@SerialkompatIgnore // internal scratch type, no cross-version contract
data class DebugDump(
val raw: String,
)
Both modes fail safe. OPT_IN never checks a type you haven't reviewed. OPT_OUT never
skips a type just because you forgot to annotate it. Adding or removing an annotation takes
effect on the next run.
Next¶
- Configuration: the full
serialkompat { }DSL. - Monorepos: scope each module's check.
- Persisted data & history: check against every recorded release.