Working with Protobuf Oneof Fields in Golang

Krishna Iyer Easwaran

November 29, 2019

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

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

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 the union 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 the oneof interface. Always remember this convention when working with oneofs.

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.

References

  1. Official Protobuf Documentation