Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
21 Jul 2025 · 5 min read ·Article 43 / 110
Go

43 Leveraging `oneof` in Protobuf

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

Protobuf (Protocol Buffers) is extremely popular as a cross-platform serialization format that is both space-efficient and fast. When building protocol messages between services, we often run into the need for a single field that can hold several different data types (similar to the union or sum type pattern found in other languages). This is exactly where the powerful oneof feature becomes our go-to weapon. This article walks through applying oneof in Protobuf with code examples, simulations, and even a flow diagram.


Greater Expressive Power

In Protobuf, each field is usually a simple type: string, int, message, enum. But life isn’t always that simple! Sometimes an API response, event, or command needs a field that can take on several possible types at once—for example, a result might be an error, data, or a status.

Properties like this are hard to achieve by relying solely on a handful of optional fields—and when you do, the UI/UX on the consumer side turns into chaos: “Which field is filled in, and which one is valid?”

Here is a simple illustration:

proto
1// Not recommended!
2message PlainResponse {
3  string error = 1;
4  Data data = 2;
5  Status status = 3;
6}

Confusing, right? Is error being set, or data, or maybe both at once? It’s entirely possible all three are empty.


The oneof Concept: Elegant & Safe

This is where Protobuf introduces the oneof feature to represent a disjoint union:

proto
1message OneOfResponse {
2  oneof result {
3    string error = 1;
4    Data data = 2;
5    Status status = 3;
6  }
7}

With this, ONLY one field (or none at all) may be set at any given time. Protobuf enforces this validity during both encoding and decoding.


Case Study: Messaging System

Let’s bring this into a real-world scenario. Take a simple chat system where the two main message types are text and image. It makes far more sense to define the message structure as:

proto
 1syntax = "proto3";
 2
 3message TextMessage {
 4  string text = 1;
 5}
 6
 7message ImageMessage {
 8  string url = 1;
 9  string caption = 2;
10}
11
12message ChatMessage {
13  string sender = 1;
14  int64 timestamp = 2;
15  oneof content {
16    TextMessage text_msg = 3;
17    ImageMessage image_msg = 4;
18  }
19}

With the definition above, the consumer is guaranteed to receive only one type of message content at a time—it’s impossible for a message to contain both text and image, or for both to be null (unless the message genuinely has no content).


Simulation: Implementation in Golang

Let’s simulate how we interact with Protobuf in Golang (assuming protoc has already generated the Go code):

go
 1chat := &messaging.ChatMessage{
 2    Sender:    "Alicia",
 3    Timestamp: time.Now().Unix(),
 4    Content: &messaging.ChatMessage_TextMsg{
 5        TextMsg: &messaging.TextMessage{
 6            Text: "Good morning!",
 7        },
 8    },
 9}
10
11bin, err := proto.Marshal(chat)
12// ... send over gRPC or save to DB
13
14// Deserialize
15var recv messaging.ChatMessage
16err = proto.Unmarshal(bin, &recv)
17switch content := recv.Content.(type) {
18case *messaging.ChatMessage_TextMsg:
19    fmt.Println("Text message:", content.TextMsg.Text)
20case *messaging.ChatMessage_ImageMsg:
21    fmt.Println("Image message:", content.ImageMsg.Url)
22default:
23    fmt.Println("Unknown message type")
24}

Using oneof makes the content field an interface type, so with an idiomatic Go-style switch it’s very easy to determine the actual data type.


Comparison Table: Classical Optional Fields VS. oneof

CriteriaOptional Fieldsoneof
Validating a single set fieldMust be done manuallyAutomatic via Protobuf
Ease of parsing in codeMore complex, lots of if-elseDirect pattern match/switch
Size on the wire/protobufAll fields may be set/garbageOnly one field is encoded
Type representation modelRedundant, ambiguousSimilar to enum/disjoint-union

Serialization Process Flow Diagram

Let’s visualize the oneof serialization flow below:

MERMAID
flowchart TD
    A(User Membuat ChatMessage) --> B{Isi Field content?}
    B -- TextMessage --> C1[Isi field text_msg]
    B -- ImageMessage --> C2[Isi field image_msg]
    B -- Kosong --> D[Simpan message tanpa content]
    C1 --> E[Protobuf Serialize]
    C2 --> E
    D --> E
    E --> F[Kirim via network/gRPC]

Side Benefits

  • Schema Evolution: Adding a new type is as simple as adding one entry to the oneof, without disrupting consumers.
  • Interoperability: Helps protobuf consumers across languages understand the semantics precisely.
  • Compatibility: This kind of structure provides excellent backward compatibility (as long as you don’t change/rename field numbers!).

Tips & Best Practices

  1. Always Use Named oneof
    Clear names (e.g., content, result) make the schema easy to read.
  2. Document Every Member
    Add a description to each field to avoid misuse.
  3. Use Versioning
    If you want to add new data types in the future, use new field numbers.
  4. Reference Sub-messages
    Avoid stuffing many different primitives into a single oneof.

Anti-pattern: Nested oneof Overuse

Don’t nest oneof recursively too deeply unless there’s a specific use case for it. Doing so makes messages hard to debug and maintain.


Conclusion

Protobuf oneof brings the concept of a neo-union to the level of microservices, APIs, and message queue systems. It prevents the emergence of overlapping field patterns, keeps the data model semantic, and is easy to parse. If you’re designing a protocol that requires a “field that may hold only one value”, don’t hesitate to use oneof.

The more you leverage this pattern in protobuf, the cleaner, more robust, and more future-proof your codebase becomes.


Bonus:
Official documentation source: Protobuf oneof .


Happy experimenting, and may your protocol architecture grow ever more solid! 🚀


References

Related Articles

💬 Comments