Repository navigation
Description
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:
- Generator limitation: Existing templates cannot generate gRPC support for any service whose request/response messages reference external types that already have a
ToProtoimplementation ingoogle-cloud-gax-internal(such asgoogle.rpc.RetryInfoinshowcase, though we do not care about showcase). - Release-compatibility blind spot: If we later qualify
.to_proto()calls atHEAD, workspace CI would pass even when a newToProtoimplementation ingoogle-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
- Qualify
ToProtocalls (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 aToProtoimplementation for a shared model type in the future. - 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 ingoogle-cloud-gax-internal) would break compatibility with older released crate versions, allowing us to evaluate the affected versions and risk before publishing. - Isolate conversion traits per crate (longer-term): Alternatively, move away from a shared public
gaxi::prost::ToPrototrait for external model types in favor of crate-private conversion traits or functions so implementations do not leak across crate boundaries.
Activity
test(protobuf): add ToProto type inference canary
chore: regenerate ToProto conversions
suzmue commentedon Nov 4, 2026
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.
cleanup(internal/sidekick/rust): qualify ToProto calls (#7897)
chore: regenerate FromProto conversions
cleanup(internal/sidekick/rust): qualify FromProto calls (#7909)
chore: update librarian and regenerate qualified To/FromProto (#7374)
test: add ToProto/FromProto canary
test: add ToProto/FromProto canary
fix: qualify ToProto calls on foreign receivers (#7437)
test: add ToProto/FromProto canary