What Is an Enum?
An enum (short for "enumeration") is a type that has a fixed set of possible values. Instead of using magic numbers or arbitrary strings, you give each possible value a clear, readable name.
Real-world analogy: Think of a traffic light. It can only be one of three states: Red, Yellow, or Green. You wouldn't represent it as a number (1, 2, 3) or a free-text string ("whatever you want") — you'd use a fixed list of options. That's exactly what an enum does.
Without enums, you might write something like this:
message Order {
string order_id = 1;
int32 status = 2; // What does 3 mean? Who knows!
}
With enums, your intent is crystal clear:
message Order {
string order_id = 1;
OrderStatus status = 2; // Can only be PENDING, SHIPPED, DELIVERED, etc.
}
2. Your First Protobuf Enum
Here is the simplest possible enum in a .proto file:
syntax = "proto3";
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0; // Default value (REQUIRED at position 0)
ORDER_STATUS_PENDING = 1;
ORDER_STATUS_PROCESSING = 2;
ORDER_STATUS_SHIPPED = 3;
ORDER_STATUS_DELIVERED = 4;
ORDER_STATUS_CANCELLED = 5;
}
Let's break down each part:
| Part | Example | What It Means |
|---|
enum | enum OrderStatus | Declares a new enum type called OrderStatus |
| Name | ORDER_STATUS_PENDING | A human-readable label for this value |
| Number | = 1 | The actual number sent over the wire (NOT the index!) |
| Zero value | ORDER_STATUS_UNSPECIFIED = 0 | The default — every enum MUST have a value at 0 |
Key point: The numbers (= 0, = 1, = 2...) are what actually get sent over the network. The names (ORDER_STATUS_PENDING) are only for humans reading the code. On the wire, protobuf sends just the number.
3. The Zero Value Rule
In proto3, there is one rule you must never break:
Rule: Every enum must have a value with number 0, and it must be the first value listed. This is the default value.
Why does this rule exist?
In proto3, all fields have default values. For integers, the default is 0. For strings, it's "". For enums, the default is whatever value has number 0. If you don't define a 0 value, protobuf wouldn't know what the default should be.
enum Color {
COLOR_RED = 1; // No 0 value!
COLOR_GREEN = 2;
COLOR_BLUE = 3;
}
enum Color {
COLOR_UNSPECIFIED = 0; // 0 is first
COLOR_RED = 1;
COLOR_GREEN = 2;
COLOR_BLUE = 3;
}
Best practice: Name the 0 value as SOMETHING_UNSPECIFIED. This makes it clear that the field hasn't been explicitly set. Think of it like a dropdown menu with "Please select..." as the first option.
4. Naming Conventions
Protobuf has specific naming conventions for enums. Following them is not just style — some tools and language generators depend on them.
| What | Convention | Example |
|---|
| Enum type name | PascalCase | OrderStatus |
| Enum value names | UPPER_SNAKE_CASE | ORDER_STATUS_PENDING |
| Value name prefix | Must match enum type name | ORDER_STATUS_ for OrderStatus |
| Zero value suffix | _UNSPECIFIED | ORDER_STATUS_UNSPECIFIED |
Why prefix every value with the enum name?
In some programming languages (like C and C++), enum values exist in the global scope. If you had two enums with a value called UNKNOWN, they would collide. The prefix prevents this:
| ✘ BAD — names can collide |
|---|
enum Color {
UNSPECIFIED = 0;
RED = 1;
}
enum Size {
UNSPECIFIED = 0; // Collision!
SMALL = 1;
}
enum Color {
COLOR_UNSPECIFIED = 0;
COLOR_RED = 1;
}
enum Size {
SIZE_UNSPECIFIED = 0;
SIZE_SMALL = 1;
}
5. How Enums Map to C#
When protobuf generates C# code from your .proto file, each protobuf enum becomes a C# enum. The prefix is stripped automatically to follow C# conventions.
Proto definition:
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0;
ORDER_STATUS_PENDING = 1;
ORDER_STATUS_PROCESSING = 2;
ORDER_STATUS_SHIPPED = 3;
ORDER_STATUS_DELIVERED = 4;
ORDER_STATUS_CANCELLED = 5;
}
Generated C# code (auto-generated, you never write this):
public enum OrderStatus
{
Unspecified = 0,
Pending = 1,
Processing = 2,
Shipped = 3,
Delivered = 4,
Cancelled = 5,
}
How you use it in C#:
// Creating a message with an enum value
var order = new Order
{
OrderId = "ORD-12345",
Status = OrderStatus.Pending
};
// Reading the enum value
if (order.Status == OrderStatus.Shipped)
{
Console.WriteLine("Your order is on its way!");
}
// Switching on enum values
switch (order.Status)
{
case OrderStatus.Unspecified:
Console.WriteLine("Status not set yet");
break;
case OrderStatus.Pending:
Console.WriteLine("Waiting for processing");
break;
case OrderStatus.Processing:
Console.WriteLine("Being prepared");
break;
case OrderStatus.Shipped:
Console.WriteLine("On its way");
break;
case OrderStatus.Delivered:
Console.WriteLine("Arrived!");
break;
case OrderStatus.Cancelled:
Console.WriteLine("Order was cancelled");
break;
}
// Converting between enum and int
int statusNumber = (int)order.Status; // 1
var status = (OrderStatus)3; // OrderStatus.Shipped
// Converting to/from string
string statusName = order.Status.ToString(); // "Pending"
6. Using Enums in Messages
Enums are used as field types inside messages, just like int32 or string:
syntax = "proto3";
enum Priority {
PRIORITY_UNSPECIFIED = 0;
PRIORITY_LOW = 1;
PRIORITY_NORMAL = 2;
PRIORITY_HIGH = 3;
PRIORITY_CRITICAL = 4;
}
enum PaymentMethod {
PAYMENT_METHOD_UNSPECIFIED = 0;
PAYMENT_METHOD_CREDIT_CARD = 1;
PAYMENT_METHOD_DEBIT_CARD = 2;
PAYMENT_METHOD_BANK_TRANSFER = 3;
PAYMENT_METHOD_CRYPTO = 4;
}
message Order {
string order_id = 1;
OrderStatus status = 2; // Enum as a field type
Priority priority = 3; // Another enum
PaymentMethod payment = 4; // And another
}
You can also use enums in repeated fields (lists):
message FilterRequest {
// "Show me orders that are PENDING or SHIPPED"
repeated OrderStatus statuses = 1;
}
In C#, this becomes:
var filter = new FilterRequest();
filter.Statuses.Add(OrderStatus.Pending);
filter.Statuses.Add(OrderStatus.Shipped);
7. Nested Enums (Inside a Message)
You can define an enum inside a message. This is useful when the enum is only relevant to that specific message.
syntax = "proto3";
message Product {
string name = 1;
double price = 2;
Category category = 3;
// This enum is scoped to Product
enum Category {
CATEGORY_UNSPECIFIED = 0;
CATEGORY_ELECTRONICS = 1;
CATEGORY_CLOTHING = 2;
CATEGORY_FOOD = 3;
CATEGORY_BOOKS = 4;
}
}
To use a nested enum from another message, use the Parent.EnumName syntax:
message OrderItem {
string product_name = 1;
int32 quantity = 2;
Product.Category category = 3; // Reference nested enum
}
In C#, nested enums become inner types:
// The nested enum is accessed as Product.Types.Category
var item = new OrderItem
{
ProductName = "Laptop",
Quantity = 1,
Category = Product.Types.Category.Electronics
};
When to nest vs. top-level?
Nest it when the enum only makes sense within the context of one message (e.g., Product.Category).
Keep it top-level when multiple messages will use the same enum (e.g., OrderStatus used in Order, OrderResponse, OrderFilter, etc.).
8. Enum Aliases (Two Names, Same Value)
Sometimes you want two different names to represent the same value. For example, PRIORITY_NORMAL and PRIORITY_DEFAULT might mean the same thing.
By default, protobuf does not allow this — you must explicitly opt in with allow_alias = true:
enum Priority {
option allow_alias = true; // Required to enable aliases
PRIORITY_UNSPECIFIED = 0;
PRIORITY_LOW = 1;
PRIORITY_NORMAL = 2;
PRIORITY_DEFAULT = 2; // Alias: same value as NORMAL
PRIORITY_HIGH = 3;
PRIORITY_CRITICAL = 4;
PRIORITY_URGENT = 4; // Alias: same value as CRITICAL
}
enum Priority {
// Missing: option allow_alias = true;
PRIORITY_UNSPECIFIED = 0;
PRIORITY_NORMAL = 2;
PRIORITY_DEFAULT = 2; // Error!
}
enum Priority {
option allow_alias = true;
PRIORITY_UNSPECIFIED = 0;
PRIORITY_NORMAL = 2;
PRIORITY_DEFAULT = 2; // OK!
}
In C#, both names exist and are interchangeable:
// Both lines do exactly the same thing
var p1 = Priority.Normal;
var p2 = Priority.Default;
Console.WriteLine(p1 == p2); // True
Console.WriteLine((int)p1); // 2
Console.WriteLine((int)p2); // 2
When to use aliases: When you're renaming an enum value but need to keep the old name for backward compatibility, or when two names are genuinely synonymous in your domain.
9. Reserved Values (Safe Deletion)
When you remove an enum value from your .proto file, there's a danger: someone might accidentally reuse the old number or name in the future, which would break compatibility with old data.
Real-world analogy: Imagine a company retires employee badge number 42. If they give badge 42 to a new employee, old security logs would now point to the wrong person. Instead, they should "retire" the number permanently.
The reserved keyword does exactly this:
enum PaymentMethod {
PAYMENT_METHOD_UNSPECIFIED = 0;
PAYMENT_METHOD_CREDIT_CARD = 1;
PAYMENT_METHOD_DEBIT_CARD = 2;
PAYMENT_METHOD_BANK_TRANSFER = 3;
PAYMENT_METHOD_CRYPTO = 4;
// These values were removed but reserved to prevent accidental reuse
reserved 5, 6;
reserved "PAYMENT_METHOD_PAYPAL", "PAYMENT_METHOD_CASH";
}
Now if anyone tries to use number 5 or the name PAYMENT_METHOD_PAYPAL, the protobuf compiler will give an error.
You can reserve:
- Numbers:
reserved 5, 6, 10; - Ranges:
reserved 5 to 10; - Names:
reserved "OLD_NAME_1", "OLD_NAME_2";
Important: You cannot mix numbers and names in a single reserved statement. Use separate lines.
10. How Enums Travel on the Wire
On the wire, enums are sent as varint-encoded integers — exactly like int32. The name is never sent; only the number is.
| Enum Value | Number Sent | Bytes on Wire |
|---|
ORDER_STATUS_UNSPECIFIED | 0 | 0 bytes (default, not sent) |
ORDER_STATUS_PENDING | 1 | 1 byte |
ORDER_STATUS_PROCESSING | 2 | 1 byte |
ORDER_STATUS_SHIPPED | 3 | 1 byte |
ORDER_STATUS_DELIVERED | 4 | 1 byte |
ORDER_STATUS_CANCELLED | 5 | 1 byte |
Since enums use varint encoding:
- Values 0–127 use 1 byte
- Values 128–16,383 use 2 bytes
- The default value (0) uses 0 bytes — it's simply not sent
Tip: Keep your enum numbers small (under 128) to use just 1 byte per value. In practice, most enums have fewer than 20 values, so this is almost never a concern.
11. What Happens with Unknown Enum Values?
Imagine you add a new value ORDER_STATUS_REFUNDED = 6 to your enum, but the client hasn't been updated yet. The client receives a message with value 6 — what happens?
In proto3: Unknown enum values are preserved. The field stores the raw integer, even though it doesn't match any known name.
// Server sends OrderStatus = 6 (REFUNDED)
// Client doesn't know about REFUNDED yet
var order = Order.Parser.ParseFrom(data);
// The raw value is preserved
int rawValue = (int)order.Status; // 6
// But it won't match any known enum name
// order.Status.ToString() returns "6" (the number as a string)
// You can check if the value is defined
bool isDefined = Enum.IsDefined(typeof(OrderStatus), order.Status);
// isDefined = false (client doesn't know about 6)
Why this matters: This is what makes protobuf forward-compatible. Old clients don't crash when they receive new enum values — they just preserve the raw number. This is critical for systems where you can't update all clients and servers at the same time.
12. Complete Working Example
Here's a complete example: a .proto file, the generated service, and the C# server implementation.
12.1 The Complete Proto File (enums.proto)
syntax = "proto3";
option csharp_namespace = "IndepthProtobuf";
package enums;
// --- Enums ---
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0;
ORDER_STATUS_PENDING = 1;
ORDER_STATUS_PROCESSING = 2;
ORDER_STATUS_SHIPPED = 3;
ORDER_STATUS_DELIVERED = 4;
ORDER_STATUS_CANCELLED = 5;
}
enum Priority {
option allow_alias = true;
PRIORITY_UNSPECIFIED = 0;
PRIORITY_LOW = 1;
PRIORITY_NORMAL = 2;
PRIORITY_DEFAULT = 2;
PRIORITY_HIGH = 3;
PRIORITY_CRITICAL = 4;
PRIORITY_URGENT = 4;
}
enum PaymentMethod {
PAYMENT_METHOD_UNSPECIFIED = 0;
PAYMENT_METHOD_CREDIT_CARD = 1;
PAYMENT_METHOD_DEBIT_CARD = 2;
PAYMENT_METHOD_BANK_TRANSFER = 3;
PAYMENT_METHOD_CRYPTO = 4;
reserved 5, 6;
reserved "PAYMENT_METHOD_PAYPAL", "PAYMENT_METHOD_CASH";
}
message Product {
string name = 1;
double price = 2;
Category category = 3;
enum Category {
CATEGORY_UNSPECIFIED = 0;
CATEGORY_ELECTRONICS = 1;
CATEGORY_CLOTHING = 2;
CATEGORY_FOOD = 3;
CATEGORY_BOOKS = 4;
}
}
// --- Messages ---
message Order {
string order_id = 1;
OrderStatus status = 2;
Priority priority = 3;
PaymentMethod payment = 4;
repeated OrderItem items = 5;
}
message OrderItem {
string product_name = 1;
int32 quantity = 2;
Product.Category category = 3;
}
// --- Service ---
service OrderService {
rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse);
rpc UpdateOrderStatus (UpdateOrderStatusRequest) returns (UpdateOrderStatusResponse);
rpc GetOrdersByStatus (GetOrdersByStatusRequest) returns (GetOrdersByStatusResponse);
}
message CreateOrderRequest {
repeated OrderItem items = 1;
PaymentMethod payment_method = 2;
Priority priority = 3;
}
message CreateOrderResponse {
string order_id = 1;
OrderStatus status = 2;
}
message UpdateOrderStatusRequest {
string order_id = 1;
OrderStatus new_status = 2;
}
message UpdateOrderStatusResponse {
string order_id = 1;
OrderStatus status = 2;
}
message GetOrdersByStatusRequest {
OrderStatus status = 1;
}
message GetOrdersByStatusResponse {
repeated Order orders = 1;
}
12.2 C# Service Implementation (OrderService.cs)
using Grpc.Core;
using IndepthProtobuf;
namespace IndepthProtobuf.Services;
public class OrderServiceImpl : OrderService.OrderServiceBase
{
// In-memory storage for demo purposes
private static readonly List<Order> _orders = new();
public override Task<CreateOrderResponse> CreateOrder(
CreateOrderRequest request, ServerCallContext context)
{
// Validate the payment method
if (request.PaymentMethod == PaymentMethod.Unspecified)
{
throw new RpcException(new Status(
StatusCode.InvalidArgument,
"Payment method is required"));
}
// Create the order
var order = new Order
{
OrderId = $"ORD-{Guid.NewGuid().ToString()[..8]}",
Status = OrderStatus.Pending,
Priority = request.Priority == Priority.Unspecified
? Priority.Normal // Default to Normal if not specified
: request.Priority,
Payment = request.PaymentMethod
};
// Add items to the order
foreach (var item in request.Items)
{
order.Items.Add(item);
}
_orders.Add(order);
return Task.FromResult(new CreateOrderResponse
{
OrderId = order.OrderId,
Status = order.Status
});
}
public override Task<UpdateOrderStatusResponse> UpdateOrderStatus(
UpdateOrderStatusRequest request, ServerCallContext context)
{
// Find the order
var order = _orders.FirstOrDefault(o => o.OrderId == request.OrderId);
if (order == null)
{
throw new RpcException(new Status(
StatusCode.NotFound,
$"Order {request.OrderId} not found"));
}
// Validate status transition
if (request.NewStatus == OrderStatus.Unspecified)
{
throw new RpcException(new Status(
StatusCode.InvalidArgument,
"New status cannot be UNSPECIFIED"));
}
// Update the status
order.Status = request.NewStatus;
return Task.FromResult(new UpdateOrderStatusResponse
{
OrderId = order.OrderId,
Status = order.Status
});
}
public override Task<GetOrdersByStatusResponse> GetOrdersByStatus(
GetOrdersByStatusRequest request, ServerCallContext context)
{
var response = new GetOrdersByStatusResponse();
// Filter orders by status
var filtered = _orders.Where(o => o.Status == request.Status);
foreach (var order in filtered)
{
response.Orders.Add(order);
}
return Task.FromResult(response);
}
}
12.3 C# Client Example
using Grpc.Net.Client;
using IndepthProtobuf;
// Connect to the server
using var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new OrderService.OrderServiceClient(channel);
// --- Create an order ---
var createResponse = await client.CreateOrderAsync(new CreateOrderRequest
{
PaymentMethod = PaymentMethod.CreditCard,
Priority = Priority.High,
Items =
{
new OrderItem
{
ProductName = "Mechanical Keyboard",
Quantity = 1,
Category = Product.Types.Category.Electronics
},
new OrderItem
{
ProductName = "C# in Depth",
Quantity = 2,
Category = Product.Types.Category.Books
}
}
});
Console.WriteLine($"Order created: {createResponse.OrderId}");
Console.WriteLine($"Status: {createResponse.Status}");
// Output: Order created: ORD-a1b2c3d4
// Output: Status: Pending
// --- Update the order status ---
var updateResponse = await client.UpdateOrderStatusAsync(new UpdateOrderStatusRequest
{
OrderId = createResponse.OrderId,
NewStatus = OrderStatus.Shipped
});
Console.WriteLine($"Updated status: {updateResponse.Status}");
// Output: Updated status: Shipped
// --- Get all pending orders ---
var pendingOrders = await client.GetOrdersByStatusAsync(new GetOrdersByStatusRequest
{
Status = OrderStatus.Pending
});
Console.WriteLine($"Pending orders: {pendingOrders.Orders.Count}");
// --- Working with enum values ---
// Check if a status is valid
var status = OrderStatus.Shipped;
bool isValid = Enum.IsDefined(typeof(OrderStatus), status);
Console.WriteLine($"Is valid: {isValid}"); // True
// Get all possible values
foreach (OrderStatus s in Enum.GetValues(typeof(OrderStatus)))
{
Console.WriteLine($" {s} = {(int)s}");
}
// Output:
// Unspecified = 0
// Pending = 1
// Processing = 2
// Shipped = 3
// Delivered = 4
// Cancelled = 5
// Using aliases (Priority.Normal == Priority.Default)
Console.WriteLine(Priority.Normal == Priority.Default); // True
13. Common Mistakes to Avoid
Mistake 1: Forgetting the zero value
enum Status {
STATUS_ACTIVE = 1; // No 0!
STATUS_INACTIVE = 2;
}
enum Status {
STATUS_UNSPECIFIED = 0;
STATUS_ACTIVE = 1;
STATUS_INACTIVE = 2;
}
Mistake 2: Using meaningful business value at 0
enum Role {
ROLE_ADMIN = 0; // Dangerous!
ROLE_USER = 1; // Default = admin?!
}
| If someone forgets to set the field, they get ADMIN by default! | ✔ GOOD |
enum Role {
ROLE_UNSPECIFIED = 0;
ROLE_ADMIN = 1;
ROLE_USER = 2;
}
| Unset fields are clearly "unspecified," not accidentally privileged. |
Mistake 3: Not prefixing enum values
enum Color {
UNSPECIFIED = 0;
RED = 1;
BLUE = 2;
}
enum Color {
COLOR_UNSPECIFIED = 0;
COLOR_RED = 1;
COLOR_BLUE = 2;
}
Mistake 4: Deleting enum values without reserving them
enum PaymentMethod {
PAYMENT_METHOD_UNSPECIFIED = 0;
PAYMENT_METHOD_CREDIT_CARD = 1;
// Removed PAYPAL (was 2)
PAYMENT_METHOD_CRYPTO = 3;
}
| Someone could reuse number 2 for something else, breaking old data! | ✔ GOOD |
enum PaymentMethod {
PAYMENT_METHOD_UNSPECIFIED = 0;
PAYMENT_METHOD_CREDIT_CARD = 1;
PAYMENT_METHOD_CRYPTO = 3;
reserved 2;
reserved "PAYMENT_METHOD_PAYPAL";
}
Mistake 5: Not handling UNSPECIFIED in your code
// Blindly trust the enum value
ProcessPayment(request.PaymentMethod);
// Check for UNSPECIFIED first
if (request.PaymentMethod ==
PaymentMethod.Unspecified)
{
throw new RpcException(new Status(
StatusCode.InvalidArgument,
"Payment method is required"));
}
ProcessPayment(request.PaymentMethod);
14. Summary
| Feature | Syntax | When to Use |
|---|
| Basic enum | enum Name { ... } | Any fixed set of options (status, type, category) |
| Zero value | NAME_UNSPECIFIED = 0 | Always required — represents "not set" |
| Nested enum | Define inside a message | When the enum belongs to one specific message |
| Aliases | option allow_alias = true | When two names should map to the same value |
| Reserved | reserved 5, 6; | When removing values — prevents accidental reuse |
| Wire format | Varint (same as int32) | Automatic — values 0–127 use 1 byte |