How does @ProtoNumber work in kotlinx.serialization ProtoBuf, and what happens if you omit it?
answer
- Tag = field number, not name, on the wire
- Omit → auto 1,2,3 in declaration order
- Reorder/insert/delete shifts implicit numbers → silent break
- Annotate explicitly, never reuse a number, only append
- Gaps in numbers are allowed
basics
~10 s@ProtoNumber sets the field's tag number on the wire. ProtoBuf stores fields by number, not name. If you omit it, numbers are assigned automatically starting at 1 in declaration order.
solid answer
~40 sIn Protocol Buffers each field is identified on the wire by an integer **field number** (tag), not by its name. In kotlinx.serialization you set it with `@ProtoNumber(n)` on the property. These numbers are the contract between encoder and decoder: matching numbers must line up, and they must be stable across versions for compatibility. If you omit `@ProtoNumber`, kotlinx assigns numbers automatically as 1, 2, 3… in property declaration order. That auto-assignment is dangerous for schema evolution: reordering, inserting, or removing a property shifts the implicit numbers and silently breaks previously written data. So for any persisted or cross-service ProtoBuf, you annotate every field explicitly with a fixed `@ProtoNumber`, never reuse a retired number, and only append new ones. `@ProtoNumber` is `@ExperimentalSerializationApi`.
code
kotlin · 8 lines@Serializable
data class Order(
@ProtoNumber(1) val id: Long,
@ProtoNumber(2) val total: Int,
@ProtoNumber(3) val note: String? = null,
)
// Adding a new field later: give it @ProtoNumber(4), keep it nullable.
// Never renumber id, total, or note.go deeper
Knows @ProtoNumber sets a tag and that ProtoBuf is number-based.
Explains auto-assignment in declaration order and why reorder/insert breaks compatibility; applies explicit annotation.
States the full evolution discipline (never renumber/reuse, append-only, nullable additions) and ties it to silent corruption.
Establishes team conventions/lint for numbering, reserved ranges, and review gates so the wire contract stays stable across services.
## Field numbers are the identity ProtoBuf does not put field *names* on the wire. Each field is encoded with a small integer **tag** = `(field_number << 3) | wire_type`. The **field number** is what both sides agree on. The decoder reads tag 1, knows that means the first agreed field, and maps it back. ## @ProtoNumber `@ProtoNumber(n)` is the kotlinx annotation (in package `kotlinx.serialization.protobuf`) that pins a property to wire number `n`: ```kotlin import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoNumber @Serializable data class User( @ProtoNumber(1) val id: Long, @ProtoNumber(2) val name: String, @ProtoNumber(5) val email: String? = null, // gaps are fine ) ``` Numbers need not be contiguous; gaps are allowed and normal (you leave room for future fields). ## What omitting it does If you do **not** annotate, kotlinx auto-assigns `1, 2, 3, …` in **declaration order**. That works for a quick local round-trip, but ties the wire format to source order: - Reorder two properties → their numbers swap → old bytes decode into the wrong fields. - Insert a property in the middle → everything after it shifts by one. - Delete a property → all following numbers shift down. Because the decoder trusts the number, these are *silent* corruptions, not exceptions, when types happen to be compatible. ## Rules for safe evolution - **Annotate every field explicitly** in any schema that is persisted or shared. - **Never change** a field's number once data exists. - **Never reuse** a retired number for a different meaning. - **Only append** new fields with fresh numbers, and make them nullable / give defaults so old readers can skip them. ## Status `@ProtoNumber` is part of the experimental ProtoBuf API (`@ExperimentalSerializationApi`).
- Are field numbers required to be contiguous?No. Gaps are allowed and even encouraged so you can reserve ranges for future fields without renumbering existing ones.
- Why is auto-assignment risky in production?It binds the wire numbers to property declaration order, so any reorder/insert/delete silently changes the encoding and breaks already-stored or in-flight data.
Field numbers are like seat numbers on a ticket: the usher seats you by number, not by your name — change the numbering and everyone ends up in the wrong seat.
saying these in an interview costs you the question
- Thinks ProtoBuf matches fields by property name
- Says auto-assigned numbers are safe to reorder
- Proposes reusing a deleted field's number
- Believes numbers must be 1..N contiguous
- Doesn't make appended fields nullable/defaulted