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.

Leave a Comment

Your email address will not be published. Required fields are marked *