This post is going to be short and sweet. And likely a huge relief for long-time Protobuf users.
Oneofs are a disaster. The generated code for using oneofs
is awful in some languages (such as Go), and oneofs
have basic limitations — like their inability to use repeated and map fields, and backwards-compatibility issues — that make their use often impractical. Alas, there's no virtue in crying over spilled milk. So instead of continuing to whine about it, the Buf team did what it always does: we fixed it.
Instead of using oneofs
, you can now use the new (buf.validate.message).oneof
Protovalidate annotation. As long as you're validating your messages with Protovalidate (and if you aren't at this point—you should be), (buf.validate.message).oneof
does exactly what you'd expect, with none of the pain.
As an example, assume you had a UserRef
message as follows:
message UserRef {
oneof value {
string id = 1;
string name = 2;
}
}
Instead, now do:
import "buf/validate/validate.proto";
message UserRef {
option (buf.validate.message).oneof = { fields: ["id", "name"] };
string id = 1;
string name = 2;
}
What if you want to ensure that exactly one field is set, rather than at most one? Previously, you'd do:
import "buf/validate/validate.proto";
message UserRef {
oneof value {
option (buf.validate.oneof).required = true;
string id = 1;
string name = 2;
}
}
Now:
import "buf/validate/validate.proto";
message UserRef {
option (buf.validate.message).oneof = { fields: ["id", "name"], required: true };
string id = 1;
string name = 2;
}
Generated oneof
code is painful in some languages. Anyone who has used oneofs
in Go will be familiar with this nightmare:
userRef := &userv1.UserRef{
Value: &userv1.UserRef_Name{
Name: "alice",
},
}
Now? It's just a field!
userRef := &userv1.UserRef{
Name: "alice",
}
What about repeated and map fields in a oneof
? Say we wanted a list of names
instead of a single name
. Formerly, we'd have to do this:
message NameList {
repeated string values = 1;
}
message UserRef {
oneof value {
string id = 1;
NameList name_list = 2;
}
}
How does this translate to Go?
userRef := &userv1.UserRef{
Value: &userv1.UserRef_Names{
Names: &userv1.NameList{
Values: []string{"alice", "bob"},
},
},
}
Disgusting! What about in the new world? Here's your Protobuf definition:
import "buf/validate/validate.proto";
message UserRef {
option (buf.validate.message).oneof = { fields: ["id", "names"] };
string id = 1;
repeated string names = 2;
}
And it Just Works™ as you'd expect: id
can be a non-empty string, or names
can be a non-empty list, but not both. The Go code is crisp and clear:
userRef := &userv1.UserRef{
Names: []string{"alice", "bob"},
}
Want to stop using a oneof
? And then use it again? Have at it! No annoying backwards-compatibility issues to worry about: it just works, and is fully backwards- and forwards-compatible. Moving oneof
to a runtime check has a similar effect to Protovalidate's (buf.validate.field).required
validation: required is dangerous as implemented in the Protobuf spec, but works great as a runtime validation that you can add and remove as you please.
oneof
annotation. They're short and explain the exact semantics.(buf.validate.message).oneof
. Pull the latest version of the BSR module via buf dep update, and update your Protovalidate language implementation to the latest version.We hope you love this as much as we do. Happy Protobuf'ing!