42 Using Enums in Protobuf
When we build modern distributed applications, data communication between services becomes crucial, especially when our microservices are written in different programming languages. Protobuf, or Protocol Buffers, from Google, has become one of the top choices for data serialization. One of the interesting features of Protobuf is the enum, which is often overlooked but is extremely vital for keeping data consistent and type-safe.
This article will take a deep dive into how to use enums in Protobuf, complete with code examples, usage simulations, and implementation best practices. I’ll also point out a few anti-patterns that commonly come up so you’ll be better prepared when implementing them.
Why Are Enums Important in Protobuf?
Let’s start with a simple understanding: enums help you restrict the values that can be assigned to a field to a fixed set of options, making your code safer and easier to maintain.
In general, enums in protobuf:
- Improve code readability
- Make automatic data validation easier
- Ensure data integrity across programming languages
- Reduce the risk of invalid values, since the integers are handled in a standardized way
Defining Enums in Protobuf
A Simple Enum Example
Let’s look at how it’s defined in a Protobuf file:
1syntax = "proto3";
2
3package user;
4
5enum UserRole {
6 USER_ROLE_UNSPECIFIED = 0; // Convention: use 0 as the default for an undefined value
7 USER_ROLE_ADMIN = 1;
8 USER_ROLE_MEMBER = 2;
9 USER_ROLE_GUEST = 3;
10}
11
12message User {
13 string id = 1;
14 string name = 2;
15 UserRole role = 3;
16}Explanation
- USER_ROLE_UNSPECIFIED is always recommended to be the value
0. This is important, because if the value is not set, Protobuf will automatically take0as the default. - Each enum member is given a unique integer value.
- This enum can be used inside a message as a data type, like
roleabove.
Enum Mapping Across Different Languages
Let’s see how the enum from the proto code above can be compiled and used in various languages:
| Language | Enum Representation |
|---|---|
| Go | user.UserRole_USER_ROLE_ADMIN |
| Java | User.UserRole.USER_ROLE_ADMIN |
| Python | UserRole.USER_ROLE_ADMIN |
| Typescript | UserRole.USER_ROLE_ADMIN (in the generated d.ts file) |
Simulation: Using Enums in a User Service Application
Say we have a simple microservice for users. We want to fetch all users with a certain role. You’ll see how enums really help keep code consistent on both the client and server side.
Request-Response Service Simulation
Protobuf File
1service UserService {
2 rpc GetUsersByRole (GetUsersByRoleRequest) returns (GetUsersByRoleResponse);
3}
4
5message GetUsersByRoleRequest {
6 UserRole role = 1;
7}
8
9message GetUsersByRoleResponse {
10 repeated User users = 1;
11}Service Implementation (Go example)
1func (s *UserService) GetUsersByRole(ctx context.Context, req *userpb.GetUsersByRoleRequest) (*userpb.GetUsersByRoleResponse, error) {
2 var filtered []*userpb.User
3 for _, u := range s.users {
4 if u.Role == req.Role {
5 filtered = append(filtered, u)
6 }
7 }
8 return &userpb.GetUsersByRoleResponse{Users: filtered}, nil
9}Calling It from the Client
1from user_pb2 import GetUsersByRoleRequest, UserRole
2
3request = GetUsersByRoleRequest(role=UserRole.USER_ROLE_MEMBER)
4response = stub.GetUsersByRole(request)
5print(response.users)Every role value is always certain, it can never fall outside the range 0-3.
Flow Diagram of the Enum Serialization Process
Let’s visualize how an enum works during data transmission using the following mermaid code:
flowchart LR A[Client Mengirim Request dengan Enum UserRole] --> B[Protobuf Serialisasi enum ke Integer Value] B --> C[Network] C --> D[Server Menerima Integer Value] D --> E[Protobuf Mapping Integer ke Enum] E --> F[Pengolahan Data oleh Service]
Challenges and Best Practices for Enums in Protobuf
1. Avoid Changing Existing Enum Values
Once you’ve published an enum value, never change the order or its integer value. Doing so causes data conflicts, mapping errors, or even corrupted values in older messages that have already been serialized.
2. Adding New Enum Values? Safe!
Adding a new member to an enum is compatible, as long as you don’t change the integer value of existing members. If you want to deprecate one, keep the old declaration in the proto file.
3. Reserved & Deprecated
If you want to remove an enum value, use the reserved annotation:
1enum UserRole {
2 USER_ROLE_UNSPECIFIED = 0;
3 USER_ROLE_ADMIN = 1;
4 // reserved 2; // So that value 2 can't be reused for a new value
5 USER_ROLE_GUEST = 3;
6}4. Default Value Zero
There is always a 0 value as the unspecified/unknown/default, so backward compatibility is preserved.
5. String Mapping and Interoperability
Sometimes you need to map an enum to a string (for example, in the frontend). The safe practice: store the enum value in the database as a number, then render it as a string for display.
Example mapping in Go:
1func RoleToString(role userpb.UserRole) string {
2 switch role {
3 case userpb.UserRole_USER_ROLE_ADMIN:
4 return "Admin"
5 case userpb.UserRole_USER_ROLE_MEMBER:
6 return "Member"
7 case userpb.UserRole_USER_ROLE_GUEST:
8 return "Guest"
9 default:
10 return "Unspecified"
11 }
12}Common Anti-Patterns
Don’t silently repurpose a reserved enum value to mean something else! For example:
1// Wrong: abusing role 99 for "SuperAdmin"
2enum UserRole {
3 USER_ROLE_UNSPECIFIED = 0;
4 USER_ROLE_ADMIN = 1;
5 USER_ROLE_MEMBER = 2;
6 USER_ROLE_SUPER_ADMIN = 99; // Avoid jumping integers around for no reason!
7}Conclusion
Enums in Protobuf provide great power: more structured, safe, and easy-to-maintain data across languages. By understanding the best practices above and avoiding the anti-patterns, you can build a more solid and scalable application architecture without losing the flexibility to evolve your data model in the future.
Are you already using enums correctly in your Protobuf? If not, try refactoring with the examples above and feel how much easier validation and consistency become!
Further references:
Happy coding! 🚀