Skip to content

derive(Message) builds tuple-struct Default in tag order instead of declaration order #1414

Description

@meng-xu-cs

Summary

#[derive(::prost::Message)] currently sorts message fields by tag before generating several code paths. That is correct for encoding-related codegen, but for tuple structs the generated Default impl also uses that tag-sorted field list.

For tuple structs, constructor argument order must follow Rust declaration order, not protobuf tag order. When those differ, the generated Default impl can either:

  • fail to compile because the constructor arguments are emitted in the wrong positions, or
  • compile but assign defaults to the wrong tuple element when multiple fields share the same Rust type.

Reproduction

#[derive(Clone, PartialEq, ::prost::Message)]
pub struct T(
    #[prost(uint32, tag = "2")] pub u32,
    #[prost(string, tag = "1")] pub ::prost::alloc::string::String,
);

Then run:

cargo check

Expected behavior

The derive should preserve declaration order for tuple constructor arguments and generate something equivalent to:

impl ::core::default::Default for T {
    fn default() -> Self {
        T(
            0u32,
            ::prost::alloc::string::String::new(),
        )
    }
}

This should compile regardless of whether protobuf tags match tuple-field declaration order.

Actual behavior

The generated tuple Default constructor is built in tag order instead of declaration order, effectively producing the arguments in the wrong positions.

For the example above, the generated constructor is equivalent to:

T(
    ::prost::alloc::string::String::new(),
    0u32,
)

which causes a compile error similar to:

error[E0308]: arguments to this struct are incorrect
expected `u32`, found `String`
expected `String`, found `u32`

Impact

  • Valid tuple structs deriving Message can fail to compile when tags are out of declaration order.
  • In some cases this can become a silent semantic bug instead of a compile error. For example, two tuple fields may share the same Rust type while still having different defaults at the protobuf layer (for example distinct enum-typed scalar fields, both represented as i32). In that case the generated Default impl can compile while assigning defaults to the wrong slots.

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