Protocol Buffers: Defining Your Schema

gRPC — 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. namefull_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.