Protocol Buffers: Defining Your Schema

gRPC
Chapter 2 ยท Protocol Buffers: Defining Your Schema

๐Ÿ“ Protocol Buffers: Defining Your Schema

Chapter 1 showed a .proto file glimpse without explaining it. This chapter covers the real syntax โ€” messages, fields, numbering rules โ€” and why this schema-first contract is a genuine structural advantage over how most REST APIs document themselves.

The .proto File

Every gRPC schema starts with a syntax declaration and, optionally, a package name to avoid naming collisions across files.

syntax = "proto3"; package shop;

Message Types & Scalar Fields

A message defines a structured data type โ€” each field has a type, a name, and a number.

message Product { int32 id = 1; string name = 2; double price = 3; bool in_stock = 4; }
proto3 Scalar TypeRoughly Equivalent To
int32 / int64Integer (32-bit / 64-bit)
stringUTF-8 text
boolBoolean
float / doubleFloating-point number
bytesRaw binary data

Field Numbering Rules

The number after each field's = is not a default value โ€” it's the field's unique identifier in the binary wire format. The field's name only exists for humans reading the .proto file; the actual bytes sent over the network reference fields by number, never by name.

  • Numbers 1โ€“15 encode in a single byte โ€” reserve these for a message's most frequently set fields.
  • Numbers 16 and above take two bytes to encode โ€” fine for less common fields.
  • A field number, once used in any deployed version of a schema, should never be reused for a different field later โ€” old serialized data (or old clients) may still reference it.

Repeated Fields

The repeated keyword marks a field as a list โ€” the closest proto3 equivalent to a JSON array.

message Product { int32 id = 1; string name = 2; repeated string tags = 5; }

Nested Messages

A message can use another message as a field's type, building the same kind of hierarchical structure a nested JSON object would.

message Address { string city = 1; string postcode = 2; } message Customer { int32 id = 1; string name = 2; Address shipping_address = 3; }

Why Schema-First Beats REST's Often-Implicit Contract

A REST API's contract is frequently documented separately from the code that implements it โ€” an OpenAPI/Swagger spec written by hand, generated after the fact, or simply a wiki page โ€” and any of these can silently drift out of sync with what the API actually does, exactly the kind of documentation-vs-reality gap api-design1-4 touched on. A .proto file has no such gap: it's not documentation about the contract, it is the contract โ€” the exact same file that generates both the client and server code (Chapter 3), so client and server can never silently disagree about a message's shape.

REST's Contract vs. gRPC's Contract

REST (typically)

Documented separately (OpenAPI/Swagger, a wiki) โ€” can drift from the real implementation over time if not diligently kept in sync.

gRPC

The .proto file generates both sides of the contract directly โ€” client and server code both come from the same source, so they can't quietly diverge.

๐Ÿ’ป Coding Challenges

Challenge 1: Write a Message Definition

Write a .proto message called Order with fields: id (int32), customer_name (string), and a repeated list of item_names (string). Assign field numbers deliberately.

Goal: Practice combining scalar fields and a repeated field in one message definition.

โ†’ Solution

Challenge 2: Add a Nested Message

Extend Challenge 1's Order message with a nested ShippingInfo message (fields: address string, expedited bool) as a field on Order.

Goal: Practice defining and referencing a nested message type.

โ†’ Solution

Challenge 3: Diagnose a Field Numbering Mistake

A team removes a deprecated field numbered 3 from a message, then later adds a completely different new field and assigns it number 3 to "reuse" it. Explain what could go wrong.

Goal: Practice applying this chapter's field-numbering rule to a realistic schema-evolution mistake.

โ†’ Solution

โš ๏ธ Gotcha: Changing a Field's Number Breaks Compatibility โ€” Renaming It Doesn't

Because the wire format identifies fields by number, not name, renaming a field (e.g. name โ†’ full_name, keeping the same number) is completely safe โ€” old and new code still agree on what number 2 means. But changing a field's number, or reusing a retired number for something unrelated, is genuinely dangerous: any client or stored data still using the old schema will misinterpret the field, potentially reading a string where it expects an integer, with no error raised at all. When a field is truly no longer needed, mark its number reserved rather than letting it be reassigned, so no one accidentally reuses it later.

๐ŸŽฏ What's Next

The next chapter is Generating Code & Your First gRPC Service โ€” the protoc compiler, generated client/server stubs, service definition syntax, and a basic unary call end to end.