Skip to content

Got a BREAK?

A decision path for a reported break: fix it, accept it, rename it, or bump the major version. It is for the developer whose PR just failed. A BREAK means a reader throws in the stated direction: backward (new code reads old data) or forward (old code reads new data). Read the finding first: it names the type, the rule, the direction and a fix: line.

flowchart TD
    A[BREAK on your PR] --> B{Was the change intended?}
    B -- No --> F[Fix the model]
    B -- Yes --> C{Is it a type rename?}
    C -- Yes --> R[Declare a rename]
    C -- No --> D{Does any reader or stored data still depend on the old shape?}
    D -- Yes --> M[Bump the major version, then accept the break]
    D -- No --> X[Accept the break]
  • Fix it

    The change was an accident. Change the model, not the gate.

  • Accept it

    Deliberate, and nothing still depends on the old shape.

  • Declare a rename

    A type's serial name moved. Declare it, and it stops reading as remove plus add. A renamed property is still accepted by hand.

  • Bump major

    Old data or old readers can't keep working.

Fix it

Most breaks are accidents, and the report's fix: line says what to do. The common fixes, grouped by what changed. Every rule, in both directions, is in the matrix.

What changed Usual fix Rules
Fields
A required field was added Give it a default, or make it nullable with explicitNulls = false. Readers should set ignoreUnknownKeys (forward). In CBOR only a default works PROPERTY_ADDED
A field was removed Keep it (deprecate it in code) until no reader or stored data uses it PROPERTY_REMOVED
A field became required Keep a default so older payloads still decode PROPERTY_OPTIONALITY
A field became optional Annotate it @EncodeDefault(ALWAYS), set encodeDefaults = true on the writer, or keep it required PROPERTY_OPTIONALITY
A field's nullability changed Keep it nullable, or introduce a new field PROPERTY_NULLABILITY
A field's type changed Introduce a new field instead of changing the type PROPERTY_TYPE_CHANGED
A @JsonNames alias was dropped Keep the alias while any producer may still send that key PROPERTY_JSON_NAMES
Enums
A value was added JSON: enable coerceInputValues and give the reading field a default. CBOR never coerces: ship the reader first, or bump major ENUM_VALUE_ADDED
A value was removed Keep the value (deprecated) until no persisted or old data uses it ENUM_VALUE_REMOVED
Polymorphism
A subtype was added Register a default deserializer on old readers, or bump major SUBTYPE_ADDED
A subtype was removed Keep it until no old or persisted data uses it SUBTYPE_REMOVED
The discriminator key or value changed Don't change it. It breaks all polymorphic decoding DISCRIMINATOR_CHANGED, DISCRIMINATOR_VALUE_CHANGED
A property collides with the discriminator Rename the property, or set a different @JsonClassDiscriminator DISCRIMINATOR_COLLISION
Types and config
A whole type was removed Keep it while data or peers still use it CONTRACT_REMOVED
A Json { } or Cbor { } setting changed Revert it, or accept the change if every reader moves together CONFIG_NAMING_STRATEGY, CONFIG_DISCRIMINATOR, CONFIG_READER_STRICTNESS, CONFIG_ENCODE_DEFAULTS, CONFIG_EXPLICIT_NULLS, CONFIG_COERCE_INPUT, CONFIG_ARRAY_POLYMORPHISM, CONFIG_STRUCTURED_MAP_KEYS, CONFIG_SPECIAL_FLOATS, CONFIG_FORMAT, CONFIG_CHANGED, CONFIG_CBOR_BYTE_STRING, CONFIG_CBOR_DEFINITE_LENGTH, CONFIG_CBOR_TAGS (see the config and CBOR-only groups)
CBOR
A @CborLabel changed Keep it stable once labels are preferred, or bump major PROPERTY_CBOR_LABEL
@ByteString was added or removed Keep the ByteArray encoding stable; introduce a new field instead PROPERTY_BYTE_STRING
@KeyTags, @ValueTags or @ObjectTags changed Keep them stable: a reader that verifies tags rejects any other tags. Or bump major CBOR_TAGS_CHANGED
A @CborArray layout changed Only append optional elements, and only when old readers never see new data; otherwise bump major CBOR_ARRAY_LAYOUT
A @CborArray element is omitted at its default Annotate it @EncodeDefault(ALWAYS), set encodeDefaults = true, or move it to the end CBOR_ARRAY_OMITTED_DEFAULT

For example, adding a field with a default keeps old payloads decodable:

 @Serializable
 data class Order(
     val id: String,
+    val currency: String = "USD",
 )

After the fix

The finding disappears from the next run. Nothing was accepted, so the gate is as strict as before.

A COVERAGE_GAP is not a break to fix in the model. See Limitations.

Accept it

If the break is deliberate, for example every consumer has stopped using a field, add the finding to acceptedBreaks. It stops failing the build but still appears in the report, so the break stays visible. Copy the type, rule and direction from the finding.

build.gradle.kts
serialkompat {
    acceptedBreaks.set(listOf(
        "com.example.wire.Payment PROPERTY_REMOVED BACKWARD",
    ))
}

Sanctioning a deliberate break

The format is "<serialName> <RULE> [DIRECTION]":

  • the type's serial name,
  • the exact rule ID (see Rules),
  • an optional direction. Omit it to accept the finding in every direction you check, or give BACKWARD or FORWARD to accept it in only one.

An entry only covers findings with that serial name and rule. Every other finding on the type, or under that rule elsewhere, still fails. A sealed or polymorphic subtype is reported as Base/sub. Write Base/sub to accept the break under that base only, or the bare sub to accept it under every base.

These are accepted breaks in these docs. The console report calls them "acknowledged" and keeps them in a separate count, so an accepted break stays visible.

Don't accept to silence an unintended break

An accepted break is a promise that nobody still depends on the old shape. If you can't say who stopped using it, fix it or bump the major version instead. Remove the entry once the break has shipped, so the same rule isn't silently accepted again next time.

Declare a rename

A type rename reads as a removal plus an addition unless you declare it. A property rename can't be declared: serialkompat doesn't pair a removed property with an added one.

  • A type's serial name changed: declare it in renames. serialkompat then compares the two types' contents instead of reporting CONTRACT_REMOVED.

    build.gradle.kts
    serialkompat {
        renames.put("com.example.wire.LegacyOrder", "com.example.wire.OrderEvent")
    }
    
  • A property was renamed (JSON): renames is for types only. Keep the old name readable with @JsonNames, so new code still decodes old payloads.

    import kotlinx.serialization.ExperimentalSerializationApi
    import kotlinx.serialization.Serializable
    import kotlinx.serialization.json.JsonNames
    
    @OptIn(ExperimentalSerializationApi::class)
    @Serializable
    data class Order(
        @JsonNames("note") val memo: String? = null,
    )
    

The gate still reports the old name as PROPERTY_REMOVED and the new one as PROPERTY_ADDED, because it doesn't pair them. Once you've confirmed the alias covers your data, accept whichever of those the report fails, for example "com.example.wire.Order PROPERTY_REMOVED BACKWARD".

CBOR has no such bridge: @JsonNames is ignored, and a renamed field is a remove plus an add. See CBOR.

Bump major

If old data or old readers really can't keep working, ship the break as a new major version. Accept the break (above) in the same PR, and note it in your changelog. Once that PR is merged into baselineRef, later changes are compared against the new shape. If you keep history, consider whether the horizon should now start at the new major (sinceVersion).