Oneofs are a disaster. Protovalidate has fixed them.

June 13, 2025

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;
}

Why this is so much better

Generated code

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",
}

Repeated and map fields

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"},
}

Backwards-compatibility issues

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.

Where to go from here

We hope you love this as much as we do. Happy Protobuf'ing!

In this post

Ready for a trial?

Talk with an expertSign up
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.