Build a gRPC-style RPC framework (14 scenes)
Scene 03 · The schema: field numbers, not field names
A schema keys each field by a stable number, not its name — so an old reader can skip a field it doesn't know and still decode the rest. Never reuse a field number.
Previously
We can pull one whole message off the wire, but it's a meaningless blob until both sides agree how to read it — so we hand the bytes a shared schema instead of hand-rolled JSON.
Scene 03
The schema: field numbers, not field names
Diagram
A `.proto` schema card for `Greeting` sits at the top — its single source of truth for what the message contains. Below it, the encode pipeline turns `Greeting{name="Ada"}` into bytes: each field becomes a tag block (labeled by its field number, NEVER its name), a length block, and a value block. At the bottom, a reader decodes the same bytes BY NUMBER: blocks it knows turn into named slots; a field number it has never seen is read for its length and stepped over (greyed "skipped"). The schema-matrix readout names the current writer/reader versions and whether the field-reuse trap is armed.
this is the schema — the `.proto` contract for the message
Both sides of an RPC need to agree on what the bytes inside a frame MEAN — and in a fleet of services written in many languages, that agreement can't be a comment in a README. We write it down once. A *schema* is a typed contract for a message: a short file (a `.proto`) that lists each field, its type, and a stable number. A code generator turns that one file into typed client and server code in every language — no hand-rolled JSON, no per-team drift. Watch `Greeting{name="Ada"}` encode to bytes: the field becomes a tag block, then a length, then the UTF-8 'Ada'. Notice what the tag block says — it names the *field number* (`#1`), not the word 'name'. That single choice is the whole scene.
Implementation
Codec.tag
the tag packs the field number with its wire type
1def tag(field_number, wire_type):2 # one varint: number in the high bits, type in low 33 return varint(field_number << 3 | wire_type)45WIRE_VARINT = 0 # int32, bool, enum6WIRE_LEN = 2 # string, bytes, sub-message
Writer.encode
each field becomes tag, then length, then value
1def encode(msg):2 out = bytes()3 out += tag(1, WIRE_LEN) # field #1 = name4 out += varint(len(msg.name))5 out += utf8(msg.name)6 if schema >= v2: # v2 adds field #27 out += tag(2, WIRE_VARINT) # field #2 = priority8 out += varint(msg.priority)9 return out
Reader.decode
decode by number; an unknown number is stepped over
1def decode(buf):2 while buf:3 field_number, wire_type = untag(buf.read_varint())4 if field_number in self.schema:5 msg[field_number] = read_value(buf, wire_type)6 else: # number we never defined7 length = buf.read_varint()8 buf.skip(length) # step past it, no crash9 return msg
Not sure what to ask? Tap a question — the staff engineer answers in the chat panel.