Protobuf’s oneof fields accept at most one field from a set of possible fields. There are plenty of cases where this simplifies server-side implementation of messages. This short post explains this concept in (hopefully) simple terms.
Pre-requisites
To follow along with this post, the following are necessary
- Basic knowledge of Protobuf. The official documentation is a great place to start.
- Basic knowledge of Golang.
- Protoc compiler setup. Check my post on custom protoc images to get started.
- An IDE/editor of choice.
What Are Oneofs?
Protobuf Oneof
fields are special composite messages that accept at most one field from a set of possible fields.
To demonstrate this, let’s assume a server that provides some service to users where a user can be identified in one of three ways
- A Username
- An Email
- A Special User ID.
The user needs to provide one of the above identifiers and a password for authentication. Representing this as a proto message would result in the following.
message user{
string username = 1;
string email = 2;
int64 user_id = 3;
string password = 5;
}
The server now needs to check three separate fields. If the server allows multiple fields to be set, each of them needs to be validated separately and a case when one value (ex: username) is correct but another (ex: email) is wrong needs to be accounted for.
Alternatively, the server may require that only one field is set in a given message for simplicity. The server can then validate only that field. One way of accomplishing this is to document this requirement for the user
message and handle validation of cases where multiple fields are set.
The protobuf language specification provides a simple alternative for this use case; the oneof
message.
Rewriting the user
message with a oneof
yields the following.
message user{
oneof Identifier{
string username = 1;
string email = 2;
int64 user_id = 3;
}
string password = 5;
}
When this user
message is set, the identifier
field accepts only one of the 3 possibilities, thereby removing the need for a complex validation in the backend. Only the field that’s actually set needs to be validated.
For those familiar with the C programming language,
oneof
is conceptually similar to theunion
data type.
Generate
The generated go files for the oneof
message is as follows.
type User struct {
// Ignoring default fields
// ...
// Types that are assignable to Identifier:
// *User_Username
// *User_Email
// *User_UserId
Identifier isUser_Identifier `protobuf_oneof:"Identifier"`
Password string `protobuf:"bytes,5,opt,name=password,proto3" json:"password,omitempty"`
}
The Identifier
field is an interface that requires a single method.
type isUser_Identifier interface {
isUser_Identifier()
}
There are three generated structs that implement this interface, each corresponding to the three possible fields that are defined for this oneof
.
Username (string)
type User_Username struct {
Username string `protobuf:"bytes,1,opt,name=username,proto3,oneof"`
}
func (*User_Username) isUser_Identifier() {}
Email (string)
type User_Email struct {
Email string `protobuf:"bytes,2,opt,name=email,proto3,oneof"`
}
func (*User_Email) isUser_Identifier() {}
User ID (int64)
type User_UserId struct {
UserId int64 `protobuf:"varint,3,opt,name=user_id,json=userId,proto3,oneof"`
}
func (*User_UserId) isUser_Identifier() {}
As you may know with go interfaces, any struct that implements the isUser_Identifier()
method of the isUser_Identifier
interface can be used assigned to the Identifier
field. So one of the three User_xxx
field can be assigned to the Identifier
field.
Note: The generated code in go uses the convention
MessageName_OneofFiledName
to generate the concrete types that implement theoneof
interface. Always remember this convention when working withoneof
s.
Set
Setting “one of” the oneof
field is straightforward, once the types implementing the interface are known.
userWithName := &pbgen.User{
Identifier: &pbgen.User_Username{
Username: "user1",
},
Password: "test",
}
userWithEmail := &pbgen.User{
Identifier: &pbgen.User_Email{
Email: "user1@email.com",
},
Password: "test",
}
userWithUserID := &pbgen.User{
Identifier: &pbgen.User_UserId{
UserId: 1234,
},
Password: "test",
}
Get
To extract the Identifer
field from a user
message, we can switch on its type since we know that there are only three valid structs that implement the isUser_Identifier
interface.
switch id := user.Identifier.(type) {
case *pbgen.User_Username:
fmt.Println(id.Username)
case *pbgen.User_Email:
fmt.Println(id.Email)
case *pbgen.User_UserId:
fmt.Println(id.UserId)
default:
// It's fine to panic here since this branch should not be reached.
panic(fmt.Sprintf("proto: unexpected type %T", id))
}
Customization of Oneof Fields
Any custom action applied to a sub-field of a oneof
such as message validation, custom names, documentation generation etc, will not be affected by the fact that the field is a part of a one of.
For example, if a validator is set on the username
field, it will be executed if the User_Username
type is used for the Identifier
field, just as it would for the password
field.
message user{
oneof identifier{
string username = 1 [(validator.field) = {regex: "^[a-z0-9]{5,30}$"}];
string email = 2;
int64 user_id = 3;
}
string password = 5 [(validator.field) = {regex: "^[a-z0-9]{5,30}$"}];
}
Embedded messages
Oneof
fields also support embedded messages, i.e., the options for a oneof
can be messages themselves. Here’s a simple example;
// Does whatever a sub message it can.
message SubMessage{
string id = 1;
}
// Does whatever a sample message it can.
message SampleMessage {
oneof test_oneof {
string name = 1;
SubMessage sub_message = 2;
}
}
Setting the fields
sampleMessage1 := &pbgen.SampleMessage{
TestOneof: &pbgen.SampleMessage_SubMessage{
SubMessage: &pbgen.SubMessage{
Id: "test",
},
},
}
sampleMessage2 := &pbgen.SampleMessage{
TestOneof: &pbgen.SampleMessage_Name{
Name: "test",
},
}
Getting the fields
switch msg := sampleMessage.TestOneof.(type) {
case *pbgen.SampleMessage_SubMessage:
if msg.SubMessage != nil {
fmt.Println(msg.SubMessage.Id)
}
case *pbgen.SampleMessage_Name:
fmt.Println(msg.Name)
default:
panic(fmt.Sprintf("proto: unexpected type %T", msg))
}
Source
The snippets used in this post are available in the oneof folder of my go-snippets Github Repository.