skip to content

Show how to round-trip a value through ProtoBuf, and name common pitfalls (nullability, defaults, byte handling) when using binary formats.

level: middleimportance: should knowfreq 38%

answer

  1. round-trip: decode(encode(x)) == x
  2. Never String(bytes) — keep ByteArray binary
  3. Field numbers must match both sides
  4. Missing optional → use null/default
  5. Compare ByteArray with contentEquals, not ==

basics

~10 s

Encode with ProtoBuf.encodeToByteArray and decode with decodeFromByteArray of the same type. Common mistakes: treating bytes as a String, mismatched field numbers, and assuming missing optional fields will fail instead of using defaults.

solid answer

~40 s

A round-trip is `decodeFromByteArray<T>(encodeToByteArray(value))`, which should equal the original. Pitfalls: (1) **Bytes aren't text** — never `String(bytes)` or store in a varchar; keep the `ByteArray` binary (base64 only if you must put it in text). (2) **Field numbers must match** — both sides need the same `@ProtoNumber`; a mismatch silently misreads. (3) **Missing fields use defaults** — to decode payloads that may omit a field, make it nullable or give it a default, otherwise decoding can fail; ProtoBuf is lenient about absent optional fields. (4) **ByteArray equality** — `ByteArray == ByteArray` is identity, so compare with `contentEquals` in tests. (5) **decodeFromByteArray needs the right type/serializer** — pass the reified type or explicit serializer. All ProtoBuf API is `@ExperimentalSerializationApi`.

code

kotlin · 6 lines
kotlin
@Serializable
data class Msg(@ProtoNumber(1) val id: Int, @ProtoNumber(2) val text: String? = null)

val bytes = ProtoBuf.encodeToByteArray(Msg(7))
val back = ProtoBuf.decodeFromByteArray<Msg>(bytes)
// back == Msg(7, null); text was absent on the wire and fell back to its default

go deeper

for a junior

Can write encodeToByteArray/decodeFromByteArray and knows bytes aren't strings.

for a middle

Names defaults/nullability for missing fields and contentEquals for ByteArray comparison.

for a senior

Connects pitfalls to evolution/interop and enforces round-trip + golden-byte tests.

for a principal

Codifies these as conventions/lint and test fixtures so binary contracts stay correct across teams and upgrades.

## The round-trip ```kotlin @Serializable data class Profile( @ProtoNumber(1) val id: Long, @ProtoNumber(2) val name: String, @ProtoNumber(3) val nickname: String? = null, ) val original = Profile(1, "Ada") val bytes: ByteArray = ProtoBuf.encodeToByteArray(original) val restored: Profile = ProtoBuf.decodeFromByteArray(bytes) check(restored == original) ``` The reified `decodeFromByteArray<Profile>(bytes)` (or the explicit `Profile.serializer()`) tells the format which serializer to use. ## Pitfall 1 — bytes are not text `encodeToByteArray` returns a `ByteArray`. Do **not** wrap it in `String(...)`: binary data isn't valid UTF-8 and you will corrupt it. Store/transmit it as raw bytes; only if a text channel is unavoidable, base64-encode and decode symmetrically. ## Pitfall 2 — field-number mismatch ProtoBuf matches by `@ProtoNumber`. If encoder and decoder disagree on numbers, the data is silently misread (no exception when types line up). Keep numbers stable and shared. ## Pitfall 3 — defaults vs missing fields ProtoBuf is lenient about absent optional fields. To safely decode payloads that may omit a field (older writers, evolution), give it a **default** or make it **nullable** (`String? = null`). A non-null field with no default and no value on the wire is where decoding goes wrong. ## Pitfall 4 — comparing ByteArray In Kotlin `ByteArray.equals` is *reference* equality. In tests assert with `contentEquals` (or compare the decoded objects), never `==` on the arrays. ```kotlin assertTrue(expected.contentEquals(actual)) ``` ## Pitfall 5 — type/serializer `decodeFromByteArray` must know the target type. Use the reified inline overload or pass `T.serializer()`. Decoding into the wrong type yields garbage or an exception. ## Status All of this is under `@ExperimentalSerializationApi`; opt in and keep round-trip tests so wire behaviour stays verified across upgrades.

  • Why does `expected == actual` fail in a ByteArray test even when contents match?
    ByteArray uses reference equality for ==. Use contentEquals for value comparison, or compare the decoded objects instead.
  • How do you let a decoder tolerate a field that older producers never wrote?
    Make that property nullable or give it a default value so its absence on the wire is filled in rather than failing.

saying these in an interview costs you the question

  • Wraps the ByteArray in String() to transport it
  • Uses == to compare ByteArrays in tests
  • Assumes a missing optional field throws by default
  • Forgets to keep @ProtoNumber consistent across services
  • Decodes without specifying the target type/serializer

context