Skip to content

Protobuf Data Types Explained: A Practical Guide to enum, repeated, map, and oneof ​

Protobuf Naming Convention Best Practices: An Illustration of the Importance of Naming Conventions from Code Review of 50+ Proto Files

Introduction: Why are data types important? ​

Choosing the wrong data type can waste bandwidth in minor cases and cause compatibility issues in severe cases.

In a Code Review, I found an interesting case: a field clearly only needed to store numbers from 0 to 100, yet it used int64; another field needs to store binary data, but used string. Even more absurdly, someone wrote in an enum kPreLoad = 1, the enum value surprisingly doesn't start from 0.

These seemingly minor choices become difficult to change once the Proto file goes live—field numbers cannot be changed, and the client and server need to be upgraded in sync, at great cost.

This article summarizes a Protobuf data type usage guide based on observations from this Code Review. We hope it helps you choose correctly the first time and avoid pitfalls.

Statement: Pick the right type, and you’ll have half as many bugs. ​

Choosing data types is the first step in designing a Proto file. If you choose the wrong types, all subsequent code will go wrong accordingly. This is not a trivial matter—it is a key step that determines interface quality.

1. Scalar Types Quick Reference ​

Protobuf provides a rich set of scalar types; when choosing a type, consider:Storage space, performance, language compatibility.

1.1 Integer Types ​

TypeStorage spaceApplicable scenarios
int32Variable length (1-5 bytes)In most scenarios, negative number encoding is inefficient.
int64Variable length (1-9 bytes)Large numbers or timestamps
uint32Variable length (1–5 bytes)non-negative numbers
uint64Variable length (1-9 bytes)non-negative large numbers
sint32Variable length (1-5 bytes)Scenarios with many negative numbers (ZigZag encoding)
sint64Variable length (1-9 bytes)Scenarios with many negative numbers (ZigZag encoding)
fixed324 bytesMore space-efficient when the value > 2^28
fixed648 bytesMore space-efficient when the value > 2^56

Selection Recommendations ​

  • Most scenarios: use int32

  • 时间戳: use int64

  • ID/Count/Enumeration: use uint32(non-negative)

  • There are many negative numbers.: Use sint32/sint64

Practical case ​

protobuf
// ✅ Correct example
message UserInfo {
    uint32 user_id = 1;        // ID uses uint32
    uint32 user_num = 2;       // Use uint32 for the ID
    int64 created_at = 3;      // Timestamp uses int64
    int32 status = 4;          // State uses int32
}

// ❌ Incorrect example
message UserInfo {
    string user_id = 1;        // Using strings for IDs wastes space.
    int32 created_at = 2;      // Timestamps use int32 and will overflow in 2038!
}

1.2 String Type ​

TypeDescriptionApplicable scenarios
stringUTF-8 encoded textPlain text, JSON, XML
bytesbinary dataFile contents, encrypted data, serialized data

Selection advice ​

  • text data: use string

  • binary data: use bytes

  • Uncertain: use bytes(Safer, better compatibility)

Practical case studies ​

protobuf
// ✅ Correct example
message FileInfo {
    string file_name = 1;      // Use string for the filename
    bytes file_content = 2;    // Use bytes for the file content
    string checksum = 3;       // Checksum (hex string) uses string
}

// ❌ Incorrect example
message FileInfo {
    bytes file_name = 1;       // Using bytes for the filename is unreasonable.
    string file_content = 2;   // Use string for binary content; it may be garbled.
}

II. Enumeration (enum) ​

2.1 Basic Usage ​

Enums are used to represent a fixed set of values.

Specification requirements:

  • Enumeration values must start from 0 Start

  • Each enumeration value must have a clear meaning.

  • It is recommended that the first enumeration value be XXX_UNSPECIFIED = 0

Correct example ​

protobuf
enum RoleType {
    ROLE_TYPE_UNSPECIFIED = 0;
    ROLE_TYPE_ADMIN = 1;
    ROLE_TYPE_GUEST = 2;
}

enum Status {
    STATUS_UNSPECIFIED = 0;
    STATUS_ACTIVE = 1;
    STATUS_INACTIVE = 2;
}

Error example ​

protobuf
enum RoleType {
    ADMIN = 1;        // Don't start from 0!
    GUEST = 2;
}

enum RoleType {
    ADMIN = 0;        // Starting from 0, but without UNSPECIFIED
    GUEST = 1;
}

2.2 Enumeration values must start from 0 ​

Why?

  • In Proto3, enum value 0 is the default value.

  • If the field is not set, 0 will be returned

  • If 0 is not defined, it will cause parsing to fail.

2.3 Naming Prefixes for Enumeration Values ​

Avoid value name conflicts between different enums:

protobuf
enum RoleType {
    ROLE_TYPE_UNSPECIFIED = 0;
    ROLE_TYPE_ADMIN = 1;
    ROLE_TYPE_GUEST = 2;
}

enum TaskType {
    TASK_TYPE_UNSPECIFIED = 0;
    TASK_TYPE_IMPORT = 1;
    TASK_TYPE_EXPORT = 2;
}

2.4 Nested Enumerations ​

Enums can be nested in message Internal, for use only within the current Message:

protobuf
message AddUserRequest {
    enum RoleType {
        ROLE_TYPE_UNSPECIFIED = 0;
        ROLE_TYPE_ADMIN = 1;
        ROLE_TYPE_GUEST = 2;
    }
    RoleType role_type = 1;
}

When using it, you need to reference the parent:AddUserRequest.RoleType

III. Composite Types ​

3.1 repeated: array/list ​

repeated Indicates that a field can contain multiple values (similar to an array).

Specification requirements:

  • Field name usage plural form(user_ids,paths)

  • Avoid excessively large repeated fields (which may cause performance issues)

Correct example ​

protobuf
message UserListResponse {
    repeated UserInfo users = 1;          // Plural naming
    repeated string user_ids = 2;
}

message MultiPathRequest {
    repeated string paths = 1;
}

Error example ​

protobuf
message UserListResponse {
    repeated UserInfo user = 1;           // Singular naming is prone to misunderstanding.
    repeated string user_id = 2;
}

Practical Case Studies ​

protobuf
message ListUsersResponse {
    CommonResponse result = 1;
    repeated UserInfo user_list = 2;      // Clear list naming
    uint32 total_count = 3;               // Total
}

3.2 map: dictionary/mapping ​

map<key, value> Represents a collection of key-value pairs.

Specification requirements:

  • Key can only be integer or string Type

  • Value can be any type (including message)

  • Field name usage singular form(user_map,config_map)

Correct example ​

protobuf
message TaskListResponse {
    map<uint64, TaskInfo> task_map = 1;   // key is ID, value is details
}

message ConfigResponse {
    map<string, string> configs = 1;      // key is the configuration name, value is the value
}

Practical Cases ​

protobuf
message ListTaskResponse {
    CommonResponse result = 1;
    map<uint64, TaskInfo> task_map = 2;   // Use map to directly index task details
}

Choosing between map and repeated ​

SceneRecommend
Need fast lookup by keymap
Traverse in orderrepeated
Need to deduplicatemap
Simple listrepeated

IV. Nested Types ​

4.1 Internal enum ​

Enumeration nested in message Internal:

protobuf
message AddUserRequest {
    enum RoleType {
        ROLE_TYPE_UNSPECIFIED = 0;
        ROLE_TYPE_ADMIN = 1;
        ROLE_TYPE_GUEST = 2;
    }
    RoleType role_type = 1;
}

Reference the parent when using:

go
req := &pb.AddUserRequest{
    RoleType: pb.AddUserRequest_ROLE_TYPE_ADMIN,
}

4.2 Internal message ​

Define the helper type in the parent:

protobuf
message TeamInfo {
    uint32 team_id = 1;

    message MemberInfo {
        string role = 1;
        string member_id = 2;
    }

    MemberInfo first_member = 2;
    MemberInfo second_member = 3;
}

Reference the parent when used:

go
info := &pb.TeamInfo{
    FirstMember: &pb.TeamInfo_MemberInfo{
        Role: "leader",
        MemberId: "member-001",
    },
}

4.3 Reasonable Boundaries of Nesting ​

Nested typeSuggestion
internal enum✅ When an enum is only used within the parent, nesting is recommended.
internal message⚠️ Use sparingly unless it is a strongly dependent auxiliary type.
Service nesting❌Prohibited, must be defined at the top level
Request/Response nesting❌Prohibited, must be defined at the top level

V. Special Types ​

5.1 optional: optional field ​

In Proto3, all fields are by default Optional of (returns the default value when not set).optional can let you explicit Flag whether the field is set.

Use cases ​

protobuf
message UpdateUserRequest {
    string user_id = 1;
    optional string user_name = 2;       // Optional field, can be used to check whether it is set
    optional int64 update_time = 3;      // Optional field, can be used to check whether it is set
}

optional vs default value ​

go
// When using optional, you can determine whether a field is set.
if req.UserName != nil {
    // The user explicitly set user_name
}

// Without optional, it is impossible to distinguish between "set to an empty string" and "not set".
// The default value "" will be considered as "not set"

5.2 bytes: binary data ​

bytes Used to store arbitrary binary data:

protobuf
message FileChunkResponse {
    bytes chunk = 1;                 // File chunk data
}

message SecureGatewayRequest {
    bytes private_key = 1;           // Private key (binary)
    bytes public_key = 2;            // Public key (binary)
}

bytes vs string ​

TypeApplicable scenarios
stringUTF-8 text, JSON, XML
bytesBinary data (files, encrypted, compressed data)

VI. Public Type Design ​

6.1 Unified Response Structure ​

Extract the common response structure into a separate common.proto Chinese:

protobuf
// common.proto
message CommonResponse {
    int32 code = 1;
    string message = 2;
}

6.2 Import References ​

protobuf
// user.proto
import "common.proto";

message CreateUserResponse {
    CommonResponse result = 1;
    UserInfo user = 2;
}

VII. Type Selection Quick Reference Table ​

SceneRecommendation typeExplanation
ID / Numberuint32/uint64non-negative integer
timestampint64Avoid the 2038 overflow
Count / Quantityuint64May exceed 2^32
Status codeint32The range is small, int32 is sufficient.
textstringUTF-8
binarybytesarbitrary data
Fixed value setenumHighly readable
listrepeatedarray
key-value pairmapQuick search
Optional fieldoptionalExplicit flag not set

Summary ​

Data type selection is the cornerstone of Proto design. If you choose wrong, compatibility issues will follow the entire project lifecycle. Once launched, field numbers and types cannot be easily changed.

Three core principles:

  • Choose the appropriate integer type: No wasted space, no overflow

  • Enumeration starts from 0: Proto3 default value requirements

  • Nesting should be moderate.: Internal enum is allowed; Request/Response and Service must not be nested

Finally, I’d like to share a sentence with everyone:Choose the right types and you halve the bugs; choose the wrong types and the refactoring never ends.

Further reading ​

This is the second article in the Protobuf series.

📖 Previous article:Protobuf Naming Convention Best Practices: The Importance of Naming Conventions as Seen from Code Reviews of 50+ Proto Files

📖 Next article:Protobuf File Organization Guidelines: A Practical Guide to Splitting, Imports, and Common Type Management

Last updated on11/07/2026, 02:16 PM
@fishfinal | @fishfinals | [email protected]