Limitations¶
What serialkompat cannot analyse, what to do about it, and the known limits. It is for
anyone who sees a COVERAGE_GAP warning, or wonders why a type isn't being checked.
Findings are judged per direction: backward (new code reads old data)
and forward (old code reads new data). A type that can't
be analysed can't be judged in either.
When a type can't be analysed¶
Some types can't be analysed. The extractor doesn't skip these or guess. It records each
one as an OPAQUE contract: present in the snapshot, diffable, and round-trippable, but
flagged. Examples:
- a
@Contextualfield whose type has no serializer in the module you pointed serialkompat at, and no fallback (a builtin likeInstant, or a@Serializableclass). serialkompat resolves contextual fields the way the runtime does, so a registered or fallback serializer is analysed normally; with neither, a real encode throws too. - an open polymorphic base with no visible subtypes, for example because it isn't
registered in the
SerializersModuleyou pointed serialkompat at. Its subtypes are only knowable from the module, so an empty list can't be trusted. - a type whose serializer can't load, such as a field type missing from the runtime
classpath (
NoClassDefFoundError) or a serializer whose static initializer throws. - a generic type used with different type arguments in the same graph, such as
Page<User>andPage<Order>. Type arguments aren't part of a contract's identity yet, so serialkompat can't check each instantiation separately. - a generic sealed hierarchy, an unrecognized
SerialKind, or a class file that can't be read.
This is a core guarantee: the extractor never throws on a model it can't fully
analyse, and unanalysable never means safe. The classifier reports every opaque type as
a COVERAGE_GAP WARN, never a silent pass.
A type that was opaque in the baseline but is analysed now (after a code change, or a
newer serialkompat) has no old shape to compare with, so the change can't be verified
either way. It is reported as one COVERAGE_GAP WARN, not as a removal plus an
addition. That includes a listed type that didn't exist yet at baselineRef, unless its
@SerialName differs from its class name (see
Quick start → Your first PR). The reverse, analysed to opaque, is still a CONTRACT_REMOVED BREAK plus the
gap, so lost coverage stays loud.
Note
A broken configuration is different from a hard-to-analyse model. If you set
jsonInstance and it can't be loaded, extraction fails instead of guessing with the
default Json config.
What to do about an opaque type¶
Each opaque type shows up as a COVERAGE_GAP WARN naming it. A warning doesn't fail the
build, but that type isn't being checked: treat it as work to do, not as a pass. To close
the gap, give serialkompat what it needs:
@Contextualfield: see@Contextualresolution.- Open polymorphic base: register its subtypes in that same module.
- Serializer that can't load: put the missing class on the module's runtime classpath, or fix the failing static initializer.
- Generic type with several instantiations: check a concrete wrapper type per instantiation until type arguments are part of a contract's identity.
A hand-written KSerializer whose descriptor the extractor can't see into, such as a custom
primitive (PrimitiveSerialDescriptor("Money", PrimitiveKind.STRING)), is also opaque, for
JSON and CBOR alike. If a type can't be
made analysable, record that decision by adding the COVERAGE_GAP to
acceptedBreaks like any finding.
@Contextual resolution¶
Since 0.3.1, serialkompat resolves each @Contextual field the way the runtime does:
1
The module entry. It looks the type up in the SerializersModule of the Json (or
Cbor) instance you point jsonInstance / cborInstance at.
2
The compiler's fallback. If the module has no entry, it uses the serializer the compiler
captured: a builtin such as kotlin.time.Instant, or the class's own @Serializable
serializer.
3
Neither: the field is OPAQUE and reported as a COVERAGE_GAP. A real encode would
throw here too.
Because the module entry wins, swapping a registered contextual serializer is a
PROPERTY_TYPE_CHANGED BREAK, not a warning.
Upgrading from 0.3.0 or earlier
Expect one COVERAGE_GAP WARN for each @Contextual field that is now resolved, the
first time you check against a baseline recorded by 0.3.0 or earlier. That baseline has
no analysed shape to compare with, so the warning replaces a false type change or
removal. It clears once the baseline is recorded by 0.3.1 or later. See the
changelog.
Known limits¶
- One wire format per module. A snapshot describes JSON or CBOR, never both (CBOR → One format per module). Tracked in #226.
- Per-class
@JsonIgnoreUnknownKeysis not read. Only theJsoninstance'signoreUnknownKeysis. Tracked in #227. - Coarse CBOR tag config.
CONFIG_CBOR_TAGSis coarser thanCBOR_TAGS_CHANGED. Tracked in #225. - JVM only. Extraction reads compiled JVM serializers, so a Kotlin Multiplatform module
needs a
jvm()target. - No source locations. Findings carry no file or line, so GitHub annotations attach to the run rather than a diff line (see Report formats).