Union-like Constructs in Protocol Buffers
Union-like constructs in Protocol Buffers (Protobuf) are implemented using the oneof
keyword, allowing a message to contain exactly one of multiple possible fields. This is useful for modeling mutually exclusive data structures (similar to union
in C or Either
in functional languages). This section covers oneof
syntax, use cases, and best practices with examples.
1. oneof
Syntax and Semantics
The oneof
keyword groups fields that cannot be set simultaneously. Only one field in the oneof
block can be present in a message.
Syntax
message MyMessage {
oneof my_oneof {
string field_a = 1;
int32 field_b = 2;
MyOtherMessage field_c = 3;
}
}
Key Features
- Mutual Exclusivity: Only one field in the
oneof
can be set. - Type Safety: Fields in
oneof
can be of any type (scalar, message, enum). - Serialization: Only the set field is serialized.
- Generated Code: Accessors return
bool
for presence checks (e.g.,has_field_a()
).
2. Example: Task Service with oneof
This example demonstrates a task creation service where notifications can be sent via email or SMS (but not both).
Proto File (task_service.proto
)
syntax = "proto3";
package task.v1;
service TaskService {
rpc CreateTask(CreateTaskRequest) returns (CreateTaskResponse) {}
}
message CreateTaskRequest {
string title = 1;
string description = 2;
oneof notification {
EmailNotification email = 3;
SmsNotification sms = 4;
}
}
message CreateTaskResponse {
oneof result {
SuccessResponse success = 1;
ErrorResponse error = 2;
}
}
message EmailNotification {
string email = 1;
}
message SmsNotification {
string phone_number = 1;
}
message SuccessResponse {
string task_id = 1;
TaskStatus status = 2;
}
enum TaskStatus {
TASK_STATUS_UNSPECIFIED = 0;
TASK_CREATED = 1;
NOTIFICATION_SENT = 2;
}
message ErrorResponse {
ErrorCode code = 1;
string message = 2;
}
enum ErrorCode {
ERROR_CODE_UNSPECIFIED = 0;
INVALID_INPUT = 1;
NOTIFICATION_FAILED = 2;
}
Stub Configuration (union_like_constructs_oneof.yaml
):
- service: TaskService
method: CreateTask
input:
equals:
title: Buy groceries
description: Milk, eggs, bread
email:
email: user@example.com
output:
data:
success:
task_id: TASK-456
status: TASK_CREATED
- service: TaskService
method: CreateTask
input:
equals:
title: Team meeting
description: Project sync at 3 PM
sms:
phone_number: "+14155550123"
output:
data:
success:
task_id: TASK-789
status: NOTIFICATION_SENT
- service: TaskService
method: CreateTask
input:
equals:
description: Invalid task
sms:
phone_number: invalid-phone
output:
data:
error:
code: INVALID_INPUT
message: Validation failed
errors:
- field: title
description: Title cannot be empty
- field: sms.phone_number
description: Invalid phone number format
3. Usage Examples
Example 1: Valid Email Notification Request
Request:
grpcurl -plaintext \
-d '{
"title": "Buy groceries",
"description": "Milk, eggs, bread",
"email": {
"email": "user@example.com"
}
}' \
localhost:4770 task.v1.TaskService/CreateTask
Response:
{
"success": {
"task_id": "TASK-456",
"status": "NOTIFICATION_SENT"
}
}
Example 2: Valid SMS Notification Request
Request:
grpcurl -plaintext \
-d '{
"title": "Team meeting",
"description": "Project sync at 3 PM",
"sms": {
"phone_number": "+14155550123"
}
}' \
localhost:4770 task.v1.TaskService/CreateTask
Response:
{
"success": {
"taskId": "TASK-789",
"status": "NOTIFICATION_SENT"
}
}
Example 3: SMS Notification with Error
Request:
grpcurl -plaintext \
-d '{
"title": "",
"description": "Invalid task",
"sms": {
"phone_number": "invalid-phone"
}
}' \
localhost:4770 task.v1.TaskService/CreateTask
Response:
{
"error": {
"code": "INVALID_INPUT",
"message": "Validation failed",
"errors": [
{
"field": "title",
"description": "Title cannot be empty"
},
{
"field": "sms.phone_number",
"description": "Invalid phone number format"
}
]
}
}
4. Best Practices
- Use for Mutual Exclusion: Model scenarios where only one field can be valid (e.g., payment methods, notification channels).
- Avoid Mixing with
optional
:oneof
fields are implicitly optional; avoid wrapping them inoptional
. - Document Field Semantics: Clearly specify which fields are part of the
oneof
in comments. - Handle Unknown Cases: Check for unset fields in your code (e.g.,
if request.notification_case == NONE
).
5. Common Pitfalls
- Multiple Fields Set: Setting more than one
oneof
field will result in only the last field being serialized. - Default Values: If no field is set, the
oneof
case is considered "none" (no default value). - Enum Conflicts: Ensure enum values in
oneof
blocks have unique numeric identifiers.
6. Further Reading
This pattern is widely used in APIs for payment processing, notifications, and polymorphic data structures.