Skip to content

gax-internal: adding ToProto implementations can break type inference #7284

Open

Description

@suzmue

google_cloud_gax_internal::prost::ToProto<T> is parameterized by the target prost type <T> and returns an associated type Result<Self::Output>:

pub trait ToProto<T>: Sized {
    type Output;
    fn to_proto(self) -> Result<Self::Output>;
}

Per The Rust Reference — Trait Implementation Coherence, Rust's orphan rules allow any downstream crate to implement a foreign trait (gaxi::prost::ToProto<T>) for a foreign model type (T0) as long as the type parameter T is a crate-local prost type (impl ToProto<LocalProst> for ExternalModel), and all such implementations are globally visible across the dependency graph even when LocalProst is pub(crate).

As explained in The Rust Book — Advanced Traits, "when a trait has a generic parameter, it can be implemented for a type multiple times... we would have to provide type annotations to indicate which implementation we want to use." Because to_proto(self) returns Result<Self::Output> rather than Result<T>, Rust can only infer T on an unqualified v.to_proto() call when a single ToProto<_> implementation exists for Self in the crate graph. Once a second implementation is added in an upstream crate (gax-internal or an upstream 1.x model crate), unqualified .to_proto() calls fail with E0284 unless disambiguated with fully qualified syntax (ToProto::<T>::to_proto(v)):

error[E0284]: type annotations needed
  --> src/generated/showcase/src/convert.rs:1495:45
   |
   | retry_info: self.retry_info.map(|v| v.to_proto()).transpose()?,
   |                                       ^^^^^^^^ cannot infer type of the type parameter `T` declared on the trait `ToProto`
   |
   = note: multiple `impl`s satisfying `google_cloud_rpc::model::RetryInfo: ToProto<_>` found in the following crates: `google_cloud_gax_internal`, `google_cloud_showcase_v1beta1`

Because published crates (such as google-cloud-storage, google-cloud-spanner, google-cloud-firestore, google-cloud-bigquery, and streaming GAPICs) currently emit unqualified .to_proto() calls on external model types (google_cloud_rpc::model::Status, google_cloud_iam_v1::model::Policy, google_cloud_longrunning::model::Operation, google_cloud_type::model::*, etc.), adding a ToProto implementation for any of those types in gax-internal or in an upstream 1.x model crate would be a breaking change that is not detected by semver checks (RFC 1105).

Today, adding a conflicting ToProto implementation is caught at HEAD in CI because unqualified .to_proto() calls fail to compile within the workspace. However, this creates two limitations:

  1. Generator limitation: Existing templates cannot generate gRPC support for any service whose request/response messages reference external types that already have a ToProto implementation in google-cloud-gax-internal (such as google.rpc.RetryInfo in showcase, though we do not care about showcase).
  2. Release-compatibility blind spot: If we later qualify .to_proto() calls at HEAD, workspace CI would pass even when a new ToProto implementation in google-cloud-gax-internal (or an upstream model crate) breaks already-released versions of downstream crates that still contain unqualified .to_proto() calls and allow the newer dependency version within their SemVer range.

While not an immediate blocker, this restricts how safely we can evolve google-cloud-gax-internal and expand gRPC support across the SDK.

Potential Next Steps

  1. Qualify ToProto calls (ToProto::<T>::to_proto): Update generated and hand-written conversion code to use fully qualified syntax so newly released crates are resilient if an upstream dependency adds a ToProto implementation for a shared model type in the future.
  2. Add a CI canary test for existing external types: Add a workspace test that calls .to_proto() unqualified on the external model types currently converted in released crates (e.g., google-cloud-rpc, google-cloud-iam-v1, google-cloud-longrunning, google-cloud-type, google-cloud-api, google-cloud-logging-type). This ensures CI alerts us if a change (most likely in google-cloud-gax-internal) would break compatibility with older released crate versions, allowing us to evaluate the affected versions and risk before publishing.
  3. Isolate conversion traits per crate (longer-term): Alternatively, move away from a shared public gaxi::prost::ToProto trait for external model types in favor of crate-private conversion traits or functions so implementations do not leak across crate boundaries.

Activity

  1. self-assigned this
    on Nov 3, 2026
  2. suzmue commented on Nov 4, 2026

    @suzmue
    ContributorAuthor

    This also applies to FromProto. Because our generated setters are generic over Into (e.g., .set_or_clear_foo(self.foo.map(|v| v.cnv()).transpose()?)), v.cnv() relies on there only being a single FromProto impl for that Prost type.

    I verified this by adding a second FromProto impl for prost_types::Timestamp in gaxi, which immediately broke compilation across our gRPC crates with E0283: type annotations needed. Qualifying FromProto::::cnv is a small template change and protects against that too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

type: cleanupAn internal cleanup or hygiene concern.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions