gRPC Proto File Syntax
A proto file is a plain text file with the extension .proto. It defines the data structures and functions available in a gRPC service. Every gRPC project starts here. Learning proto file syntax is the most important skill for working with gRPC.
The Anatomy of a Proto File
Every proto file has four main sections: the syntax declaration, the package name, imports, and the actual message and service definitions.
user.proto
──────────────────────────────────────────────
// 1. Syntax declaration — always first
syntax = "proto3";
// 2. Package — prevents name collisions
package com.estudy247.users;
// 3. Imports — other proto files this one depends on
import "google/protobuf/timestamp.proto";
// 4. Options — language-specific settings
option java_package = "com.estudy247.users";
option go_package = "./users";
// 5. Message definitions — your data structures
message User {
int32 id = 1;
string name = 2;
string email = 3;
}
// 6. Service definition — your functions
service UserService {
rpc GetUser (GetUserRequest) returns (User);
}
──────────────────────────────────────────────
The Syntax Declaration
The very first non-comment line must declare which version of Protocol Buffers you use. All modern gRPC projects use proto3. The older proto2 is still in use in some legacy systems but is not recommended for new projects.
syntax = "proto3"; ← required, must be the first statement
Packages
The package name works like a namespace. It prevents two different proto files from accidentally defining a message with the same name.
Without package:
File A defines: message Order { ... }
File B defines: message Order { ... }
← Conflict! Which Order is which?
With packages:
File A: package ecommerce; → ecommerce.Order
File B: package shipping; → shipping.Order
← No conflict. Both exist and are clearly different.
Message Definitions
A message is a data structure. It is the proto equivalent of a class or struct in other languages. Every field inside a message must have a type, a name, and a unique field number.
message Product {
// type name field number
int32 product_id = 1;
string title = 2;
double price = 3;
bool in_stock = 4;
int32 quantity = 5;
}
Rules for field numbers:
• Range: 1 to 536,870,911
• Numbers 1–15 use 1 byte in encoding (use for frequent fields)
• Numbers 16–2047 use 2 bytes (use for less-common fields)
• Numbers 19000–19999 are reserved — do not use them
• Once a number is used, NEVER reuse it — not even after deletion
Field Naming Conventions
Proto files use snake_case for field names. The code generator converts them to the convention of the target language automatically.
Proto file (snake_case): string first_name = 1; string last_name = 2; Generated in Java (camelCase): getFirstName() getLastName() Generated in Go (PascalCase): FirstName LastName Generated in Python (snake_case — unchanged): first_name last_name
Repeated Fields
A field marked repeated holds a list of values. An empty list and "field not set" look the same in proto3 — both result in an empty list in generated code.
message ShoppingCart {
int32 cart_id = 1;
repeated int32 item_ids = 2; ← zero or more integers
}
Generated Go code:
type ShoppingCart struct {
CartId int32
ItemIds []int32
}
Enum Types
An enum restricts a field to a fixed set of named values. The first value in every enum must be zero — this acts as the default "unknown" value.
enum OrderStatus {
ORDER_STATUS_UNKNOWN = 0; ← required zero value
ORDER_STATUS_PENDING = 1;
ORDER_STATUS_PROCESSING = 2;
ORDER_STATUS_SHIPPED = 3;
ORDER_STATUS_DELIVERED = 4;
ORDER_STATUS_CANCELLED = 5;
}
message Order {
int32 order_id = 1;
OrderStatus status = 2;
}
Naming convention: PREFIX_VALUE_NAME (avoids collisions across enums)
Nested Messages and Enums
Messages and enums can be defined inside other messages. This is useful when a type is only relevant inside one parent message.
message Order {
enum Status {
UNKNOWN = 0;
PENDING = 1;
DELIVERED = 2;
}
int32 order_id = 1;
Status status = 2;
message LineItem {
int32 product_id = 1;
int32 quantity = 2;
double unit_price = 3;
}
repeated LineItem line_items = 3;
}
Referencing nested types:
Order.Status
Order.LineItem
Reserved Fields
When you delete a field, mark its number and name as reserved. This prevents a future developer from accidentally reusing a number that old encoded messages still contain.
message User {
reserved 3, 4; ← these numbers cannot be reused
reserved "phone", "fax"; ← these names cannot be reused
int32 id = 1;
string name = 2;
// fields 3 and 4 were deleted — reserved so no one reuses them
}
Comments
Proto files support both single-line and block comments. Good comments document why a field exists, not just what it is.
// Single-line comment
/*
Multi-line comment.
Useful for documenting complex messages.
*/
message Invoice {
// Unique identifier assigned at creation. Never changes.
int32 invoice_id = 1;
// Total before tax, in the smallest currency unit (e.g. cents).
int64 subtotal_cents = 2;
}
Well-Known Types
Google ships a set of common message types called Well-Known Types. You import them instead of writing your own.
import "google/protobuf/timestamp.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/any.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/wrappers.proto";
Usage:
message Event {
string name = 1;
google.protobuf.Timestamp created_at = 2; ← date + time
google.protobuf.Duration duration = 3; ← time span
}
// Empty is used when a method takes no input or returns nothing
rpc DeleteAllLogs (google.protobuf.Empty) returns (google.protobuf.Empty);
Summary
A proto file starts with a syntax declaration, then declares a package, then defines messages and enums, and finally defines services. Field numbers are permanent identifiers — never reuse them. Use repeated for lists, enum for fixed value sets, and reserved to protect deleted fields. Well-Known Types handle common scenarios like timestamps and empty responses.
