skip to content

What does the @ExperimentalSerializationApi status of ProtoBuf/CBOR mean in practice, and how do you handle it in production code?

level: seniorimportance: should knowfreq 35%

answer

  1. @ExperimentalSerializationApi = no binary-compat guarantee + opt-in
  2. Opt in via @OptIn or compiler -opt-in/optIn
  3. Prefer local opt-in over blanket suppression
  4. Pin version, isolate behind adapter, golden byte tests
  5. @Serializable & JSON are stable; binary surface is not

basics

~20 s

Experimental means the API can change between versions and the compiler warns unless you opt in. In production you opt in deliberately, pin the library version, and wrap usage so a future change touches one place.

solid answer

~40 s

The binary formats (and several APIs like `@ProtoNumber`) are marked `@ExperimentalSerializationApi`. Kotlin's opt-in mechanism makes the compiler emit a warning/error at every call site until you opt in — either with `@OptIn(ExperimentalSerializationApi::class)` on the using declaration, or project-wide via the `-opt-in` compiler flag / Gradle `optIn` setting. Experimental means **no binary-compatibility guarantee**: signatures or wire details can change across minor versions. In production the discipline is: opt in consciously (not blanket-suppress everywhere), pin the kotlinx-serialization version, isolate format calls behind a thin adapter/boundary so an upgrade changes one file, keep round-trip tests with golden byte fixtures to catch wire changes, and read release notes before bumping. None of this affects the stable `@Serializable` model — only the format API surface is experimental.

go deeper

for a junior

Knows the compiler warns and you must opt in with @OptIn.

for a middle

Explains opt-in mechanics and that experimental means the API can change between versions.

for a senior

Adds production discipline: pinning, adapter isolation, golden tests, deliberate local opt-in.

for a principal

Sets org policy for adopting experimental APIs — risk assessment, upgrade gates, and containment so a wire change can't silently break persisted data.

## What 'experimental' means Kotlin has an **opt-in** system (`@RequiresOptIn`). kotlinx.serialization marks parts of its API with `@ExperimentalSerializationApi`, including the binary formats and annotations like `@ProtoNumber`. This signals: the API may change, and there is **no binary-compatibility guarantee** across versions. ## How the compiler reacts Touching an experimental API without opting in produces a warning (or error, depending on config). You resolve it explicitly: ```kotlin @OptIn(ExperimentalSerializationApi::class) fun encode(u: User): ByteArray = ProtoBuf.encodeToByteArray(u) ``` Or project-wide: ```kotlin // build.gradle.kts kotlin { compilerOptions { optIn.add("kotlinx.serialization.ExperimentalSerializationApi") } } ``` Prefer **local** `@OptIn` so you stay aware of every experimental touch point rather than silencing the whole project. ## Production discipline - **Opt in deliberately**, ideally close to the call, not via a blanket suppression that hides future surprises. - **Pin the version** of `kotlinx-serialization` and bump intentionally. - **Isolate** format access behind a thin adapter (one `encode`/`decode` boundary) so a breaking change is a one-file fix. - **Golden/round-trip tests**: assert `decode(encode(x)) == x` and keep byte-level fixtures so a wire change is caught by CI before it reaches consumers. - **Read release notes** when upgrading; experimental APIs are exactly where changes land. ## What is NOT experimental The `@Serializable` annotation and the core JSON format are stable. Only the binary format surface (and some advanced APIs) carry the experimental marker, so the risk is contained to that boundary.

  • Is the data written by an experimental format guaranteed stable on disk?
    No hard guarantee across versions — that is why you keep golden byte fixtures and read release notes, so a wire-format change is caught before it corrupts stored data.
  • Why prefer @OptIn over a project-wide flag?
    Local opt-in keeps every experimental usage visible and reviewable; a project-wide flag hides them and makes future breaking changes harder to locate.

saying these in an interview costs you the question

  • Thinks experimental just means 'unfinished/buggy' with no API/compat meaning
  • Suppresses opt-in globally without awareness
  • Assumes wire format is frozen across versions
  • No round-trip or fixture tests around format calls
  • Believes @Serializable itself is experimental

context