Plans for scoped enums?

35 views
Skip to first unread message

Max Kanat-Alexander

unread,
Jan 5, 2026, 8:49:25 AM (6 days ago) Jan 5
to Protocol Buffers
Hey proto team! :) The current docs say that scoped enums will be part of a future edition. Is that actually on a roadmap anywhere? I don't see it in Issues. I'm mostly just wondering if I should expect it by some vague date. 

I'm playing around with defining a configuration language based on a strict subset of textproto, and scoped enums would help a ton so that users could just write `state: ACTIVE` instead of the unintuitive `state: STATE_ACTIVE` in their config files. I know that I can accomplish that today by nesting enums inside of messages, but what I'm curious about is if the future potential implementation of scoped enums will break backwards compatibility with that in some way.

-Max

Em Rauch

unread,
Jan 5, 2026, 9:07:32 AM (6 days ago) Jan 5
to Max Kanat-Alexander, Protocol Buffers
Always nice to recognize a xoogler name 🙂

Broadly, the plan is for a future Edition number to have enums be treated as scoped by default, but with a feature setting that opts back in to current behavior for people who want to upgrade the Edition number of preexisting .proto files perfectly-behavior-preserving (behavior-preserving uprades should be tool assisted with a tool called Prototiller, whose OSS implementation is unfortunately still not stabilized yet as we are rewriting it to Go).

Scoped enums is close to the top of the candidates list for Edition 2026 which is targeted for 2026-Q3, but we don't have any firm commitments around Edition 2026 details at the moment.

We don't intend to change the enum behavior on proto2/proto3/Edition-2023/Edition-2024 files (or even support an option there to do so), as our forward Editions strategy is to generally treat the behavior of older syntax IDL inputs as fixed as the way to avoid the ecosystem problems that come from even adding new options that old impls/infrastructure won't know about. So unfortunately anyone who plans to stay on Proto3 forever won't get the new goodness, you'll need to continue to just wrap each enum in an empty message if you want to avoid the stupid value name collision problems.

> Is that actually on a roadmap anywhere? I don't see it in Issues. 

We mostly don't use Issues for tracking our roadmap work, primarily Issues is for handling external bugs/requests/discussion (unlike gRPC which has community-driven CNCF governance, Protobuf is still a Google project). If you want to open it on Issues so you can be notified when it moves forward, free to open and I'll tag the issue appropriately and we can try to remember to keep it up to date.

Thanks!

--
You received this message because you are subscribed to the Google Groups "Protocol Buffers" group.
To unsubscribe from this group and stop receiving emails from it, send an email to protobuf+u...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/protobuf/25fac6eb-2e5f-43e3-bcd2-3ec3df783b56n%40googlegroups.com.

Max Kanat-Alexander

unread,
Jan 5, 2026, 9:39:39 AM (6 days ago) Jan 5
to Em Rauch, Protocol Buffers
Ha, thanks Em! Wasn't sure anybody would recognize my name, still. :)

Scoped by default sounds good. And I think that timeline would work for me, actually. And for what I'm working on, I can pick any edition I want; it's essentially green-field work where I get to define the format.

So how would that look? Like say today I have:

message Wrapper {
    enum Foo {
        UNSPECIFIED = 1;
        BAR = 1;
    }
}

Today I would be referring to that as Wrapper::Foo::BAR, IIRC (I haven't done C++ proto in a long time) or Wrapper.Foo.BAR in Python/Java (IIRC?). What does "enum Foo" at the top level look like instead?

Basically I'm wondering: if I want to prepare for it now, can I name the empty messages in a particular way that will help me stay forward-compatible.

Oh, and prototiller sounds cool. :)

-Max

Em Rauch

unread,
Jan 5, 2026, 10:04:47 AM (6 days ago) Jan 5
to Max Kanat-Alexander, Protocol Buffers
So how would that look? Like say today I have:
message Wrapper {
    enum Foo {
        UNSPECIFIED = 1;
        BAR = 1;
    }
}
Today I would be referring to that as Wrapper::Foo::BAR, IIRC (I haven't done C++ proto in a long time) or Wrapper.Foo.BAR in Python/Java (IIRC?). What does "enum Foo" at the top level look like instead?

Scoped enums would actually require no other behavior change in many of the languages like Java, as the gencode actually uses a language enum which is already scoped anyway. So in Java you're just going to get `Foo.BAR` like you want without us having to do much special there.

The C++ case is actually fairly odd today, basically what happens with the wrapping type is that there's first an enum which manually prefixes everything with the nested scope basically like this (the C++ enum semantic is that all of the  constants inside are not scoped by the containing enum)

enum Wrapper_Foo : int {
   Wrapper_Foo_UNSPECIFIED = 0;
   Wrapper_Foo_BAR = 1;
}

And then later the actual message class has `using` statements that expose the type and names under that scape, like:

class Wrapper {
   using Foo = Wrapper_Foo;
   static constexpr Foo UNSPECIFIED = Wrapper_Foo_UNSPECIFIED;
   static constexpr Foo BAR = Wrapper_Foo_BAR;
}

So today it is possible to write the enum type as either `Wrapper_Foo` or `Wrapper::Foo`, and possible to name the values as either `Wrapper::BAR` or `Wrapper_Foo_Bar`. You can see that this still would have the name collisions if you had 2 enums nested in the same message and the value names collided, since the values are also 'directly' underneath `Wrapper` without any `Foo` in them there.

C++ the language added scoped enums all the way back in C++11 (they look like `enum class Foo {}`) so its far overdue for us to be able to have the gencode use it there, then we can start to emit it as:

enum class Foo : int {
   UNSPECIFIED = 0;
   BAR = 1;
}

And you can finally write Foo::UNSPECIFIED like you naturally want to.

All that to say, with scoped enums you will want to put them at top level instead of synthetically-nested in a message and the 'spelling'  won't match. It's also a possibility that we could have some sort of shim migration mode to facilitate people trying to incrementally move off the nested-message-to-scope-pattern onto proper-scoped-enums but I'm not exactly sure what it could look like to be a super smooth migration.

Em Rauch

unread,
Jan 6, 2026, 9:21:34 AM (5 days ago) Jan 6
to Max Kanat-Alexander, Protocol Buffers
Go is the other big language that doesn't have scoped-enum concept in the language, by virtue of not even having enum as a first class concept in the language. GoProto prefixes values which would partially mitigate, but IIRC it would also break at least in some cases today if we simply relaxed the scoping restriction rule.

Just FYI protoc will defensively prevent you from making enums which are in the scope with enum values whose name collide in the same .proto file. Formally we might say that "the values not being scoped to their containing enum is the .proto IDL semantic, unrelated to any language", pragmatically the main reason we don't relax it is to prevent you from accidentally making a definition that would break in C++Proto. I think to disable that check you would have to vendor and patch protoc to remove it, and any downstream implications of that would obviously be on you at that point.

As long as you don't actually create such a value name collision in the same .proto file protoc won't stop you though (it doesn't enforce the enum value name prefixing or anything, that's just a style guide to try to avoid collisions)



On Mon, Jan 5, 2026 at 11:46 PM Max Kanat-Alexander <avatr...@gmail.com> wrote:
Okay, that makes sense. So basically, my enums won't change their format in code when they are scoped, but they will behave correctly in C++. Is C++ the only language with the problem? I don't expect to need a C++ parser any time soon, so if that's the only place where it's a problem today, maybe I will just make top-level enums now and switch them to scoped when 2026 comes out.

-Max

Max Kanat-Alexander

unread,
Jan 6, 2026, 9:34:34 AM (5 days ago) Jan 6
to Em Rauch, Protocol Buffers
Okay, good to know. That doesn't sound so bad in Go; it sounds harder to have a naming conflict, but still possible.

So, I suppose the question is: does that IDL semantic need to persist in Edition 2026 if all protos are scoped? Like, should that IDL constraint only exist if the feature is set to a pre-2026 value?

-Max

Max Kanat-Alexander

unread,
Jan 6, 2026, 9:34:38 AM (5 days ago) Jan 6
to Em Rauch, Protocol Buffers
Okay, that makes sense. So basically, my enums won't change their format in code when they are scoped, but they will behave correctly in C++. Is C++ the only language with the problem? I don't expect to need a C++ parser any time soon, so if that's the only place where it's a problem today, maybe I will just make top-level enums now and switch them to scoped when 2026 comes out.

-Max

Em Rauch

unread,
Jan 6, 2026, 9:48:59 AM (5 days ago) Jan 6
to Max Kanat-Alexander, Protocol Buffers
> Like, should that IDL constraint only exist if the feature is set to a pre-2026 value?

Definitely; the semantic will effectively be:
  • protoc will resolve if the individual enum is resolved as scoped or not (scoped = 2026 files or later, unless they affirmatively set the legacy unscoped option for compatibility).
    • If it is resolved as unscoped, protoc will try to check for value name collisions between sibling enums, and generators are considered 'permitted' to emit the values unscoped.
    • If it is resolved as scoped, protoc will not check for value collisions, and generators will be required to always emit values scoped (GoProto will just need to prefix the individual values with the enum name in all cases to guarantee the scoping).

Max Kanat-Alexander

unread,
Jan 6, 2026, 9:55:10 AM (5 days ago) Jan 6
to Em Rauch, Protocol Buffers
That sounds fantastic, thank you Em. :)

-Max
Reply all
Reply to author
Forward
0 new messages