Skip to content

Feature Request: Omit enum zero-value variants with a specific suffix and represent them as Option #1314

@Jason5Lee

Description

@Jason5Lee

Motivation

buf has a ENUM_ZERO_VALUE_SUFFIX lint rule, which requires this zero value to have a specific suffix, like _UNSPECIFIED.

I agree with this practice, as it provides a safe default value if a field is not explicitly set.

The Problem in Rust

Currently, prost generates a Rust enum that includes this _UNSPECIFIED variant. For example, STATUS_UNSPECIFIED becomes Status::Unspecified.

While correct, this is often un-idiomatic and cumbersome in Rust. Having a concrete Unspecified variant forces developers to write boilerplate match arms to handle a case that, in most business logic, should be filtered out during validation.

// Current situation requires handling the zero-value case explicitly
match message.status() { // status() returns Status
    Status::Active => { /* do something */ },
    Status::Inactive => { /* do something else */ },
    Status::Unspecified => unreachable!("should not pass validation"),
}

Proposed Solution

I propose a new configuration option in prost-build that would allow prost to treat these suffixed zero-value enum variants as None.

When this option is configured (e.g., config.enum_zero_value_suffix("_UNSPECIFIED")), prost-build would change its generation strategy for matching enums:

  1. Omit the Variant: The zero-value variant (e.g., Unspecified) would be omitted from the generated Rust enum definition.
  2. Use Option<T>: Any method of a message that corresponding to the enum field returns Option<MyEnum>.
  3. Map Zero to None: a 0 value for the enum field would be coded as None.

This would make the resulting Rust code much more ergonomic and idiomatic.

Example

1. Protobuf Definition (.proto)

syntax = "proto3";

message MyMessage {
  Status status = 1;
}

enum Status {
  STATUS_UNSPECIFIED = 0;
  STATUS_ACTIVE = 1;
  STATUS_INACTIVE = 2;
}

2. Current prost Output

#[derive(Clone, Copy, Debug, PartialEq, Eq, /* ... */)]
#[repr(i32)]
pub enum Status {
    Unspecified = 0, // This variant is present
    Active = 1,
    Inactive = 2,
}

#[derive(Clone, PartialEq, ::prost::Message)]
pub struct MyMessage {
    #[prost(enumeration="Status", tag="1")]
    pub status: i32, // Field is i32, requires manual conversion and handling of 0
}

impl MyMessage {
    pub fn status() -> Status { ... }
}

3. Desired prost Output (with this feature enabled)

// The Unspecified variant is gone
#[derive(Clone, Copy, Debug, PartialEq, Eq, /* ... */)]
#[repr(i32)]
pub enum Status {
    Active = 1,
    Inactive = 2,
}

#[derive(Clone, PartialEq, ::prost::Message)]
pub struct MyMessage {
    #[prost(enumeration="Status", tag="1")]
    pub status: i32,
}

impl MyMessage {
    pub fn status() -> Option<Status> { ... }
}

Now, when I need to process an incoming MyMessage, I now can call status(), responds an invalid argument error if it is None, then passing Status around, not needing to worrying about unspecified case when matching.

Benefits

  • Idiomatic Rust: Aligns Protobuf's concept of a default/unset enum value with Rust's Option.
  • Reduced Boilerplate: Eliminates the need for match arms for _UNSPECIFIED variants.
  • Improved Ergonomics: Working with these enums becomes much more natural for Rust developers.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions