Repository: googleapis/google-cloud-rust
Pull request #1946: refactor(wkt): refactor `wkt::message::Message` trait to use new design
Author: login: chrischiedo
Created: 2025-04-28T15:36:47Z | Merged: 2025-04-30T16:26:52Z

This PR is part of #1609.

[Design doc](https://docs.google.com/document/d/1g57ChBKHKuBDxHY8GLrDf61h0m3MLLtSvKKYjFh1P1Q/edit?tab=t.0#heading=h.psi98t855m9r).

What the PR does:

- Split up the old `Message` trait into two new traits (`Message` and `MessageSerializer<T>`)
- The `MessageSerializer<T>` is hidden, as it's an internal API that's not intended for direct use by consumers of the `wkt` crate.

**Note**:

- Even though in the initial design, we had proposed the `serializer()` function to have the following signature (using _static dispatch_):
```rust
fn serializer() -> impl MessageSerializer<Self> {
    DefaultSerializer::<Self>::new()
}
```
In this PR, I've decided to go with an approach that would allow dynamic dispatch through trait objects:

```rust
fn serializer() -> Box<dyn MessageSerializer<Self>> {
    Box::new(DefaultSerializer::<Self>::new())
}
```

The dynamic dispatch approach generally allows for more cleaner code (but I'm open to feedback on this during review).

- In order to make the code simpler (and solve for some compiler warnings), I chose to make `MessageSerializer` public and hide it using `#[doc(hidden)]`, instead of giving it the `pub(crate)` visibility.
- Clippy complains about the naming of `from_map()` function: `methods called `from_* ` usually take no `self``. I'm not sure if that should warrant us changing the name of the function.


This is my first take on this refactoring task. Sharing early to get feedback. I'll be waiting to make any recommended changes.

cc: @dbolduc @coryan 


Diff:
diff --git a/src/gax/src/error/rpc.rs b/src/gax/src/error/rpc.rs
index ef4da5bbf2..c562d35d17 100644
--- a/src/gax/src/error/rpc.rs
+++ b/src/gax/src/error/rpc.rs
@@ -710,7 +710,7 @@ mod test {
     #[test_case(RetryInfo::default(), StatusDetails::RetryInfo(RetryInfo::default()))]
     fn status_from_rpc_status_known_detail_type<T>(detail: T, want: StatusDetails)
     where
-        T: wkt::message::Message + serde::ser::Serialize,
+        T: wkt::message::Message + serde::ser::Serialize + serde::de::DeserializeOwned,
     {
         let input = rpc::model::Status::default()
             .set_code(Code::Unavailable as i32)
diff --git a/src/lro/src/details.rs b/src/lro/src/details.rs
index 1354bf8177..89f59042a9 100644
--- a/src/lro/src/details.rs
+++ b/src/lro/src/details.rs
@@ -24,8 +24,8 @@ pub(crate) fn handle_start<R, M>(
     result: Result<Operation<R, M>>,
 ) -> (Option<String>, PollingResult<R, M>)
 where
-    R: wkt::message::Message + serde::de::DeserializeOwned,
-    M: wkt::message::Message + serde::de::DeserializeOwned,
+    R: wkt::message::Message + serde::ser::Serialize + serde::de::DeserializeOwned,
+    M: wkt::message::Message + serde::ser::Serialize + serde::de::DeserializeOwned,
 {
     match result {
         Err(e) => (None, PollingResult::Completed(Err(e))),
@@ -41,8 +41,8 @@ pub(crate) fn handle_poll<R, M>(
     result: Result<Operation<R, M>>,
 ) -> (Option<String>, PollingResult<R, M>)
 where
-    R: wkt::message::Message + serde::de::DeserializeOwned,
-    M: wkt::message::Message + serde::de::DeserializeOwned,
+    R: wkt::message::Message + serde::ser::Serialize + serde::de::DeserializeOwned,
+    M: wkt::message::Message + serde::ser::Serialize + serde::de::DeserializeOwned,
 {
     match result {
         Err(e) => {
@@ -85,8 +85,8 @@ where
 
 fn handle_common<R, M>(op: Operation<R, M>) -> (Option<String>, PollingResult<R, M>)
 where
-    R: wkt::message::Message + serde::de::DeserializeOwned,
-    M: wkt::message::Message + serde::de::DeserializeOwned,
+    R: wkt::message::Message + serde::ser::Serialize + serde::de::DeserializeOwned,
+    M: wkt::message::Message + serde::ser::Serialize + serde::de::DeserializeOwned,
 {
     if op.done() {
         let result = as_result(op);
@@ -99,7 +99,7 @@ where
 
 fn as_result<R, M>(op: Operation<R, M>) -> Result<R>
 where
-    R: wkt::message::Message + serde::de::DeserializeOwned,
+    R: wkt::message::Message + serde::ser::Serialize + serde::de::DeserializeOwned,
     M: wkt::message::Message + serde::de::DeserializeOwned,
 {
     if let Some(any) = op.response() {
@@ -114,7 +114,7 @@ where
 fn as_metadata<R, M>(op: Operation<R, M>) -> Option<M>
 where
     R: wkt::message::Message + serde::de::DeserializeOwned,
-    M: wkt::message::Message + serde::de::DeserializeOwned,
+    M: wkt::message::Message + serde::ser::Serialize + serde::de::DeserializeOwned,
 {
     op.metadata().and_then(|a| a.try_into_message::<M>().ok())
 }
diff --git a/src/lro/src/lib.rs b/src/lro/src/lib.rs
index dd0c64ab48..9f202dc2ac 100644
--- a/src/lro/src/lib.rs
+++ b/src/lro/src/lib.rs
@@ -131,8 +131,10 @@ pub fn new_poller<ResponseType, MetadataType, S, SF, Q, QF>(
     query: Q,
 ) -> impl Poller<ResponseType, MetadataType>
 where
-    ResponseType: wkt::message::Message + serde::de::DeserializeOwned + Send,
-    MetadataType: wkt::message::Message + serde::de::DeserializeOwned + Send,
+    ResponseType:
+        wkt::message::Message + serde::ser::Serialize + serde::de::DeserializeOwned + Send,
+    MetadataType:
+        wkt::message::Message + serde::ser::Serialize + serde::de::DeserializeOwned + Send,
     S: FnOnce() -> SF + Send + Sync,
     SF: std::future::Future<Output = Result<Operation<ResponseType, MetadataType>>>
         + Send
@@ -218,8 +220,10 @@ where
 impl<ResponseType, MetadataType, S, SF, P, PF> Poller<ResponseType, MetadataType>
     for PollerImpl<ResponseType, MetadataType, S, SF, P, PF>
 where
-    ResponseType: wkt::message::Message + serde::de::DeserializeOwned + Send,
-    MetadataType: wkt::message::Message + serde::de::DeserializeOwned + Send,
+    ResponseType:
+        wkt::message::Message + serde::ser::Serialize + serde::de::DeserializeOwned + Send,
+    MetadataType:
+        wkt::message::Message + serde::ser::Serialize + serde::de::DeserializeOwned + Send,
     S: FnOnce() -> SF + Send + Sync,
     SF: std::future::Future<Output = Result<Operation<ResponseType, MetadataType>>>
         + Send
diff --git a/src/wkt/src/any.rs b/src/wkt/src/any.rs
index 0c837ef54d..6e948acfe1 100644
--- a/src/wkt/src/any.rs
+++ b/src/wkt/src/any.rs
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+use crate::message::MessageSerializer;
+
 /// `Any` contains an arbitrary serialized protocol buffer message along with a
 /// URL that describes the type of the serialized message.
 ///
@@ -101,16 +103,17 @@ impl Any {
     /// also supports serialization to JSON.
     pub fn try_from<T>(message: &T) -> Result<Self, Error>
     where
-        T: serde::ser::Serialize + crate::message::Message,
+        T: crate::message::Message + serde::ser::Serialize + serde::de::DeserializeOwned,
     {
-        let value = message.to_map()?;
+        let serializer = T::serializer();
+        let value = serializer.serialize_to_map(message)?;
         Ok(Any(value))
     }
 
     /// Extracts (if possible) a `T` value from the [Any].
     pub fn try_into_message<T>(&self) -> Result<T, Error>
     where
-        T: serde::de::DeserializeOwned + crate::message::Message,
+        T: crate::message::Message + serde::ser::Serialize + serde::de::DeserializeOwned,
     {
         let map = &self.0;
         let r#type = map
@@ -119,7 +122,9 @@ impl Any {
             .ok_or_else(|| "@type field is missing or is not a string".to_string())
             .map_err(Error::deser)?;
         Self::check_typename(r#type, T::typename())?;
-        T::from_map(map)
+
+        let serializer = T::serializer();
+        serializer.deserialize_from_map(map)
     }
 
     fn check_typename(got: &str, want: &str) -> Result<(), Error> {
@@ -136,11 +141,10 @@ impl crate::message::Message for Any {
     fn typename() -> &'static str {
         "type.googleapis.com/google.protobuf.Any"
     }
-    fn to_map(&self) -> Result<crate::message::Map, AnyError> {
-        crate::message::to_json_other(self)
-    }
-    fn from_map(map: &crate::message::Map) -> Result<Self, AnyError> {
-        crate::message::from_other(map)
+
+    #[allow(private_interfaces)]
+    fn serializer() -> impl crate::message::MessageSerializer<Self> {
+        crate::message::ValueSerializer::<Self>::new()
     }
 }
 
diff --git a/src/wkt/src/duration.rs b/src/wkt/src/duration.rs
index f75c8a204d..846d27d8a4 100644
--- a/src/wkt/src/duration.rs
+++ b/src/wkt/src/duration.rs
@@ -164,11 +164,10 @@ impl crate::message::Message for Duration {
     fn typename() -> &'static str {
         "type.googleapis.com/google.protobuf.Duration"
     }
-    fn to_map(&self) -> Result<crate::message::Map, crate::AnyError> {
-        crate::message::to_json_other(self)
-    }
-    fn from_map(map: &crate::message::Map) -> Result<Self, crate::AnyError> {
-        crate::message::from_other(map)
+
+    #[allow(private_interfaces)]
+    fn serializer() -> impl crate::message::MessageSerializer<Self> {
+        crate::message::ValueSerializer::<Self>::new()
     }
 }
 
diff --git a/src/wkt/src/message.rs b/src/wkt/src/message.rs
index b5860f4a2e..ba3f6eef96 100644
--- a/src/wkt/src/message.rs
+++ b/src/wkt/src/message.rs
@@ -20,31 +20,85 @@ use crate::AnyError as Error;
 /// A trait that must be implemented by all messages.
 ///
 /// Messages sent to and received from Google Cloud services may be wrapped in
-/// [Any][crate::any::Any]. `Any` uses a `@type` field to encoding the type
+/// [Any][crate::any::Any]. `Any` uses a `@type` field to encode the type
 /// name and then validates extraction and insertion against this type.
 pub trait Message {
     /// The typename of this message.
     fn typename() -> &'static str;
 
+    /// Returns the serializer for this message type.
     #[doc(hidden)]
-    /// Store the value into a JSON object.
-    fn to_map(&self) -> Result<Map, Error>
+    #[allow(private_interfaces)]
+    fn serializer() -> impl MessageSerializer<Self>
     where
-        Self: serde::ser::Serialize + Sized,
+        Self: serde::ser::Serialize + serde::de::DeserializeOwned,
     {
-        to_json_object(self)
+        DefaultSerializer::<Self>::new()
     }
+}
+
+/// Internal API for message serialization.
+/// This is not intended for direct use by consumers of this crate.
+pub(crate) trait MessageSerializer<T> {
+    /// Store the value into a JSON object.
+    fn serialize_to_map(&self, message: &T) -> Result<Map, Error>;
 
-    #[doc(hidden)]
     /// Extract the value from a JSON object.
-    fn from_map(map: &Map) -> Result<Self, Error>
-    where
-        Self: serde::de::DeserializeOwned,
-    {
+    fn deserialize_from_map(&self, map: &Map) -> Result<T, Error>;
+}
+
+// Default serializer that most types can use.
+pub(crate) struct DefaultSerializer<T> {
+    _phantom: std::marker::PhantomData<T>,
+}
+
+impl<T> DefaultSerializer<T> {
+    pub fn new() -> Self {
+        Self {
+            _phantom: std::marker::PhantomData,
+        }
+    }
+}
+
+impl<T> MessageSerializer<T> for DefaultSerializer<T>
+where
+    T: serde::ser::Serialize + serde::de::DeserializeOwned + Sized + Message,
+{
+    fn serialize_to_map(&self, message: &T) -> Result<Map, Error> {
+        to_json_object(message)
+    }
+
+    fn deserialize_from_map(&self, map: &Map) -> Result<T, Error> {
         from_object(map)
     }
 }
 
+// Serializes the type `T` into the `value` field.
+pub(crate) struct ValueSerializer<T> {
+    _phantom: std::marker::PhantomData<T>,
+}
+
+impl<T> ValueSerializer<T> {
+    pub fn new() -> Self {
+        Self {
+            _phantom: std::marker::PhantomData,
+        }
+    }
+}
+
+impl<T> MessageSerializer<T> for ValueSerializer<T>
+where
+    T: serde::ser::Serialize + serde::de::DeserializeOwned + Sized + Message,
+{
+    fn serialize_to_map(&self, message: &T) -> Result<Map, Error> {
+        to_json_other(message)
+    }
+
+    fn deserialize_from_map(&self, map: &Map) -> Result<T, Error> {
+        from_other(map)
+    }
+}
+
 /// Write the serialization of `T` flatly into a map.
 ///
 /// We use this for types that do not have special encodings, as defined in:
@@ -150,7 +204,10 @@ mod test {
             "b": 2,
         });
         let map = input.as_object().cloned().unwrap();
-        let test = TestMessage::from_map(&map).unwrap();
+
+        let serializer = TestMessage::serializer();
+        let test = serializer.deserialize_from_map(&map).unwrap();
+
         assert!(test._unknown_fields.get("@type").is_none(), "{test:?}");
     }
 }
diff --git a/src/wkt/src/rstruct.rs b/src/wkt/src/rstruct.rs
index 47c63b06e0..895c6189ac 100644
--- a/src/wkt/src/rstruct.rs
+++ b/src/wkt/src/rstruct.rs
@@ -44,11 +44,10 @@ impl crate::message::Message for Struct {
     fn typename() -> &'static str {
         "type.googleapis.com/google.protobuf.Struct"
     }
-    fn to_map(&self) -> Result<crate::message::Map, crate::AnyError> {
-        crate::message::to_json_other(self)
-    }
-    fn from_map(map: &crate::message::Map) -> Result<Self, crate::AnyError> {
-        crate::message::from_other(map)
+
+    #[allow(private_interfaces)]
+    fn serializer() -> impl crate::message::MessageSerializer<Self> {
+        crate::message::ValueSerializer::<Self>::new()
     }
 }
 
@@ -56,11 +55,10 @@ impl crate::message::Message for Value {
     fn typename() -> &'static str {
         "type.googleapis.com/google.protobuf.Value"
     }
-    fn to_map(&self) -> Result<crate::message::Map, crate::AnyError> {
-        crate::message::to_json_other(self)
-    }
-    fn from_map(map: &crate::message::Map) -> Result<Self, crate::AnyError> {
-        crate::message::from_other(map)
+
+    #[allow(private_interfaces)]
+    fn serializer() -> impl crate::message::MessageSerializer<Self> {
+        crate::message::ValueSerializer::<Self>::new()
     }
 }
 
@@ -68,11 +66,10 @@ impl crate::message::Message for ListValue {
     fn typename() -> &'static str {
         "type.googleapis.com/google.protobuf.ListValue"
     }
-    fn to_map(&self) -> Result<crate::message::Map, crate::AnyError> {
-        crate::message::to_json_other(self)
-    }
-    fn from_map(map: &crate::message::Map) -> Result<Self, crate::AnyError> {
-        crate::message::from_other(map)
+
+    #[allow(private_interfaces)]
+    fn serializer() -> impl crate::message::MessageSerializer<Self> {
+        crate::message::ValueSerializer::<Self>::new()
     }
 }
 
diff --git a/src/wkt/src/timestamp.rs b/src/wkt/src/timestamp.rs
index 8fe2ffbb90..40fb740cd4 100644
--- a/src/wkt/src/timestamp.rs
+++ b/src/wkt/src/timestamp.rs
@@ -169,11 +169,10 @@ impl crate::message::Message for Timestamp {
     fn typename() -> &'static str {
         "type.googleapis.com/google.protobuf.Timestamp"
     }
-    fn to_map(&self) -> Result<crate::message::Map, crate::AnyError> {
-        crate::message::to_json_other(self)
-    }
-    fn from_map(map: &crate::message::Map) -> Result<Self, crate::AnyError> {
-        crate::message::from_other(map)
+
+    #[allow(private_interfaces)]
+    fn serializer() -> impl crate::message::MessageSerializer<Self> {
+        crate::message::ValueSerializer::<Self>::new()
     }
 }
 
diff --git a/src/wkt/src/wrappers.rs b/src/wkt/src/wrappers.rs
index 481a5199c7..6be2446c59 100644
--- a/src/wkt/src/wrappers.rs
+++ b/src/wkt/src/wrappers.rs
@@ -119,11 +119,10 @@ macro_rules! impl_message {
             fn typename() -> &'static str {
                 concat!("type.googleapis.com/google.protobuf.", stringify!($t))
             }
-            fn to_map(&self) -> Result<crate::message::Map, crate::AnyError> {
-                crate::message::to_json_other(self)
-            }
-            fn from_map(map: &crate::message::Map) -> Result<Self, crate::AnyError> {
-                crate::message::from_other(map)
+
+            #[allow(private_interfaces)]
+            fn serializer() -> impl crate::message::MessageSerializer<Self> {
+                crate::message::ValueSerializer::<Self>::new()
             }
         }
     };
@@ -157,10 +156,27 @@ impl crate::message::Message for UInt64Value {
     fn typename() -> &'static str {
         "type.googleapis.com/google.protobuf.UInt64Value"
     }
-    fn to_map(&self) -> Result<crate::message::Map, crate::AnyError> {
-        encode_string::<Self>(self.to_string())
+
+    #[allow(private_interfaces)]
+    fn serializer() -> impl crate::message::MessageSerializer<Self> {
+        UInt64ValueSerializer
+    }
+}
+
+struct UInt64ValueSerializer;
+
+impl crate::message::MessageSerializer<UInt64Value> for UInt64ValueSerializer {
+    fn serialize_to_map(
+        &self,
+        message: &UInt64Value,
+    ) -> Result<crate::message::Map, crate::AnyError> {
+        encode_string::<UInt64Value>(message.to_string())
     }
-    fn from_map(map: &crate::message::Map) -> Result<Self, crate::AnyError> {
+
+    fn deserialize_from_map(
+        &self,
+        map: &crate::message::Map,
+    ) -> Result<UInt64Value, crate::AnyError> {
         map.get("value")
             .ok_or_else(crate::message::missing_value_field)?
             .as_str()
@@ -174,10 +190,30 @@ impl crate::message::Message for Int64Value {
     fn typename() -> &'static str {
         "type.googleapis.com/google.protobuf.Int64Value"
     }
-    fn to_map(&self) -> Result<crate::message::Map, crate::AnyError> {
-        encode_string::<Self>(self.to_string())
+
+    #[allow(private_interfaces)]
+    fn serializer() -> impl crate::message::MessageSerializer<Self>
+    where
+        Self: Sized + serde::ser::Serialize,
+    {
+        Int64ValueSerializer
     }
-    fn from_map(map: &crate::message::Map) -> Result<Self, crate::AnyError> {
+}
+
+struct Int64ValueSerializer;
+
+impl crate::message::MessageSerializer<Int64Value> for Int64ValueSerializer {
+    fn serialize_to_map(
+        &self,
+        message: &Int64Value,
+    ) -> Result<crate::message::Map, crate::AnyError> {
+        encode_string::<Int64Value>(message.to_string())
+    }
+
+    fn deserialize_from_map(
+        &self,
+        map: &crate::message::Map,
+    ) -> Result<Int64Value, crate::AnyError> {
         map.get("value")
             .ok_or_else(crate::message::missing_value_field)?
             .as_str()
@@ -191,10 +227,27 @@ impl crate::message::Message for BytesValue {
     fn typename() -> &'static str {
         "type.googleapis.com/google.protobuf.BytesValue"
     }
-    fn to_map(&self) -> Result<crate::message::Map, crate::AnyError> {
-        encode_string::<Self>(STANDARD.encode(self))
+
+    #[allow(private_interfaces)]
+    fn serializer() -> impl crate::message::MessageSerializer<Self> {
+        BytesValueSerializer
+    }
+}
+
+struct BytesValueSerializer;
+
+impl crate::message::MessageSerializer<BytesValue> for BytesValueSerializer {
+    fn serialize_to_map(
+        &self,
+        message: &BytesValue,
+    ) -> Result<crate::message::Map, crate::AnyError> {
+        encode_string::<BytesValue>(STANDARD.encode(message))
     }
-    fn from_map(map: &crate::message::Map) -> Result<Self, crate::AnyError> {
+
+    fn deserialize_from_map(
+        &self,
+        map: &crate::message::Map,
+    ) -> Result<BytesValue, crate::AnyError> {
         let s = map
             .get("value")
             .ok_or_else(crate::message::missing_value_field)?
@@ -215,7 +268,7 @@ fn expected_string_value() -> crate::AnyError {
 mod test {
     use super::*;
     use crate::Any;
-    use crate::message::Message;
+    use crate::message::{Message, MessageSerializer};
     type Result = std::result::Result<(), Box<dyn std::error::Error>>;
     use test_case::test_case;
 
@@ -263,8 +316,8 @@ mod test {
     #[test_case(DoubleValue::default(), BytesValue::default())]
     fn test_wrapper_in_any_with_bad_typenames<T, U>(from: T, _into: U) -> Result
     where
-        T: Message + std::fmt::Debug + serde::ser::Serialize,
-        U: Message + std::fmt::Debug + serde::de::DeserializeOwned,
+        T: Message + std::fmt::Debug + serde::ser::Serialize + serde::de::DeserializeOwned,
+        U: Message + std::fmt::Debug + serde::ser::Serialize + serde::de::DeserializeOwned,
     {
         let any = Any::try_from(&from)?;
         assert!(any.try_into_message::<U>().is_err());
@@ -281,7 +334,10 @@ mod test {
             "@type": format!("type.googleapis.com/google.protobuf.{}", typename),
             "value": 0,
         });
-        let e = T::from_map(map.as_object().unwrap());
+
+        let serializer = T::serializer();
+        let e = serializer.deserialize_from_map(map.as_object().unwrap());
+
         assert!(e.is_err());
         let fmt = format!("{:?}", e);
         assert!(fmt.contains("expected value field to be a string"), "{fmt}");
@@ -294,7 +350,14 @@ mod test {
             "@type": "type.googleapis.com/google.protobuf.BytesValue",
             "value": "Oops, I forgot to base64 encode this.",
         });
-        assert!(BytesValue::from_map(map.as_object().unwrap()).is_err());
+
+        let serializer = BytesValue::serializer();
+        assert!(
+            serializer
+                .deserialize_from_map(map.as_object().unwrap())
+                .is_err()
+        );
+
         Ok(())
     }
 }


Reviews:
user: login: dbolduc
body: Thanks! We are getting closer...
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-28T16:20:27Z
user: login: chrischiedo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T18:56:16Z
user: login: chrischiedo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T18:57:03Z
user: login: chrischiedo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T08:31:36Z
user: login: chrischiedo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T08:33:07Z
user: login: chrischiedo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T08:33:24Z
user: login: chrischiedo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T08:33:45Z
user: login: chrischiedo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T08:34:00Z
user: login: chrischiedo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T08:34:30Z
user: login: chrischiedo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T08:35:12Z
user: login: chrischiedo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T08:35:55Z
user: login: chrischiedo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T08:37:05Z
user: login: chrischiedo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T08:37:31Z
user: login: chrischiedo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T08:56:32Z
user: login: coryan
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T13:06:13Z
user: login: dbolduc
body: This came together really nicely. I appreciate the collaboration on design + all the implementation work you did, @chrischiedo. Thanks a bunch.

Approved, in anticipation of fixing @coryan's documentation nits.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-30T14:24:59Z
user: login: chrischiedo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T15:44:00Z
user: login: chrischiedo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T15:44:52Z
user: login: coryan
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T16:03:05Z

Review comments:
diff_hunk: @@ -24,8 +24,8 @@ pub(crate) fn handle_start<R, M>(
     result: Result<Operation<R, M>>,
 ) -> (Option<String>, PollingResult<R, M>)
 where
-    R: wkt::message::Message + serde::de::DeserializeOwned,
-    M: wkt::message::Message + serde::de::DeserializeOwned,
+    R: wkt::message::Message + serde::ser::Serialize + serde::de::DeserializeOwned + 'static,
+    M: wkt::message::Message + serde::ser::Serialize + serde::de::DeserializeOwned + 'static,
path: src/lro/src/details.rs
user: login: dbolduc
body: optional: we could avoid these other changes if we separated the serialization and deserialization into two separate traits: `MessageSerializer` and `MessageDeserializer`.

don't feel obligated to do this.
created_at: 2025-04-28T16:03:40Z
author_association: MEMBER
diff_hunk: @@ -20,27 +20,52 @@ use crate::AnyError as Error;
 /// A trait that must be implemented by all messages.
 ///
 /// Messages sent to and received from Google Cloud services may be wrapped in
-/// [Any][crate::any::Any]. `Any` uses a `@type` field to encoding the type
+/// [Any][crate::any::Any]. `Any` uses a `@type` field to encode the type
 /// name and then validates extraction and insertion against this type.
 pub trait Message {
     /// The typename of this message.
     fn typename() -> &'static str;
 
+    /// Returns the serializer for this message type
     #[doc(hidden)]
-    /// Store the value into a JSON object.
-    fn to_map(&self) -> Result<Map, Error>
+    fn serializer() -> Box<dyn MessageSerializer<Self>>
path: src/wkt/src/message.rs
user: login: dbolduc
body: We should prefer static dispatch. It is more performant. We only use dynamic dispatch when needed.

For any given `T`, there is only ever one `MessageSerializer` returned, so static dispatch will work just fine.
created_at: 2025-04-28T16:05:12Z
author_association: MEMBER
diff_hunk: @@ -20,27 +20,52 @@ use crate::AnyError as Error;
 /// A trait that must be implemented by all messages.
 ///
 /// Messages sent to and received from Google Cloud services may be wrapped in
-/// [Any][crate::any::Any]. `Any` uses a `@type` field to encoding the type
+/// [Any][crate::any::Any]. `Any` uses a `@type` field to encode the type
 /// name and then validates extraction and insertion against this type.
 pub trait Message {
     /// The typename of this message.
     fn typename() -> &'static str;
 
+    /// Returns the serializer for this message type
     #[doc(hidden)]
-    /// Store the value into a JSON object.
-    fn to_map(&self) -> Result<Map, Error>
+    fn serializer() -> Box<dyn MessageSerializer<Self>>
     where
-        Self: serde::ser::Serialize + Sized,
+        Self: Sized + serde::ser::Serialize + serde::de::DeserializeOwned + 'static,
     {
-        to_json_object(self)
+        Box::new(DefaultSerializer::<Self>::new())
     }
+}
 
-    #[doc(hidden)]
-    /// Extract the value from a JSON object.
-    fn from_map(map: &Map) -> Result<Self, Error>
-    where
-        Self: serde::de::DeserializeOwned,
-    {
+/// Internal API for message serialization.
+/// This is not intended for direct use by consumers of this crate.
+#[doc(hidden)]
+pub trait MessageSerializer<T> {
path: src/wkt/src/message.rs
user: login: dbolduc
body: We do not want downstream crates to implement this, so it should be: 

```rs
pub(crate) trait MessageSerializer<T> { ... }
```

and then we will have to deal with all of the warnings from running:
```
cargo clippy -p google-cloud-wkt -- --deny warnings
```
created_at: 2025-04-28T16:07:33Z
author_association: MEMBER
diff_hunk: @@ -20,27 +20,52 @@ use crate::AnyError as Error;
 /// A trait that must be implemented by all messages.
 ///
 /// Messages sent to and received from Google Cloud services may be wrapped in
-/// [Any][crate::any::Any]. `Any` uses a `@type` field to encoding the type
+/// [Any][crate::any::Any]. `Any` uses a `@type` field to encode the type
 /// name and then validates extraction and insertion against this type.
 pub trait Message {
     /// The typename of this message.
     fn typename() -> &'static str;
 
+    /// Returns the serializer for this message type
     #[doc(hidden)]
path: src/wkt/src/message.rs
user: login: dbolduc
body: Heads up: we will also need to add a `#[allow(private_interfaces)]` annotation to this and every other `fn serializer()` after we make `MessageSerializer` private.
created_at: 2025-04-28T16:09:15Z
author_association: MEMBER
diff_hunk: @@ -20,27 +20,52 @@ use crate::AnyError as Error;
 /// A trait that must be implemented by all messages.
 ///
 /// Messages sent to and received from Google Cloud services may be wrapped in
-/// [Any][crate::any::Any]. `Any` uses a `@type` field to encoding the type
+/// [Any][crate::any::Any]. `Any` uses a `@type` field to encode the type
 /// name and then validates extraction and insertion against this type.
 pub trait Message {
     /// The typename of this message.
     fn typename() -> &'static str;
 
+    /// Returns the serializer for this message type
     #[doc(hidden)]
-    /// Store the value into a JSON object.
-    fn to_map(&self) -> Result<Map, Error>
+    fn serializer() -> Box<dyn MessageSerializer<Self>>
     where
-        Self: serde::ser::Serialize + Sized,
+        Self: Sized + serde::ser::Serialize + serde::de::DeserializeOwned + 'static,
     {
-        to_json_object(self)
+        Box::new(DefaultSerializer::<Self>::new())
     }
+}
 
-    #[doc(hidden)]
-    /// Extract the value from a JSON object.
-    fn from_map(map: &Map) -> Result<Self, Error>
-    where
-        Self: serde::de::DeserializeOwned,
-    {
+/// Internal API for message serialization.
+/// This is not intended for direct use by consumers of this crate.
+#[doc(hidden)]
+pub trait MessageSerializer<T> {
+    fn to_map(&self, message: &T) -> Result<Map, Error>;
+    fn from_map(&self, map: &Map) -> Result<T, Error>;
+}
+
+// Default serializer that most types can use
+pub(crate) struct DefaultSerializer<T> {
+    _phantom: std::marker::PhantomData<T>,
+}
+
+impl<T> DefaultSerializer<T> {
+    pub fn new() -> Self {
+        Self {
+            _phantom: std::marker::PhantomData,
+        }
+    }
+}
+
+impl<T> MessageSerializer<T> for DefaultSerializer<T>
path: src/wkt/src/message.rs
user: login: dbolduc
body: This is good.

I think we want to define another serializer in here:

```rs
// Serializes the type `T` into the `value` field.
pub(crate) struct ValueSerializer<T> {
    _phantom: std::marker::PhantomData<T>,
}

impl<T> ValueSerializer<T> {
    pub fn new() -> Self {
        Self {
            _phantom: std::marker::PhantomData,
        }
    }
}

impl<T> MessageSerializer<T> for ValueSerializer<T>
where
    T: serde::ser::Serialize + serde::de::DeserializeOwned + Sized + Message,
{
    fn to_map(&self, message: &T) -> Result<Map, Error> {
        to_json_other(message)
    }

    fn from_map(&self, map: &Map) -> Result<T, Error> {
        from_other(map)
    }
}
```
created_at: 2025-04-28T16:12:09Z
author_association: MEMBER
diff_hunk: @@ -136,10 +149,24 @@ impl crate::message::Message for Any {
     fn typename() -> &'static str {
         "type.googleapis.com/google.protobuf.Any"
     }
-    fn to_map(&self) -> Result<crate::message::Map, AnyError> {
-        crate::message::to_json_other(self)
+
+    // Override the default serializer to use custom serialization
+    fn serializer() -> Box<dyn crate::message::MessageSerializer<Self>>
+    where
+        Self: Sized,
+    {
+        Box::new(AnySerializer)
     }
-    fn from_map(map: &crate::message::Map) -> Result<Self, AnyError> {
+}
+
+struct AnySerializer;
path: src/wkt/src/any.rs
user: login: dbolduc
body: ... and then we can use `crate::message::ValueSerializer<Any>` here, instead of repeating ourselves.
created_at: 2025-04-28T16:13:52Z
author_association: MEMBER
diff_hunk: @@ -136,10 +149,24 @@ impl crate::message::Message for Any {
     fn typename() -> &'static str {
         "type.googleapis.com/google.protobuf.Any"
     }
-    fn to_map(&self) -> Result<crate::message::Map, AnyError> {
-        crate::message::to_json_other(self)
+
+    // Override the default serializer to use custom serialization
+    fn serializer() -> Box<dyn crate::message::MessageSerializer<Self>>
+    where
+        Self: Sized,
+    {
+        Box::new(AnySerializer)
     }
path: src/wkt/src/any.rs
user: login: dbolduc
body: ... so this looks more like:

```rs
    fn serializer() -> impl crate::message::MessageSerializer<Self>
    {
        crate::message::ValueSerializer::<Self>::new()
    }
```
created_at: 2025-04-28T16:14:54Z
author_association: MEMBER
diff_hunk: @@ -164,10 +164,24 @@ impl crate::message::Message for Duration {
     fn typename() -> &'static str {
         "type.googleapis.com/google.protobuf.Duration"
     }
-    fn to_map(&self) -> Result<crate::message::Map, crate::AnyError> {
-        crate::message::to_json_other(self)
+
+    // Override the default serializer to use custom serialization
+    fn serializer() -> Box<dyn crate::message::MessageSerializer<Self>>
+    where
+        Self: Sized,
+    {
+        Box::new(DurationSerializer)
+    }
+}
+
+struct DurationSerializer;
path: src/wkt/src/duration.rs
user: login: dbolduc
body: ... then same goes for `Duration`, `Timestamp`, all the types in `rstruct.rs`, etc.
created_at: 2025-04-28T16:15:38Z
author_association: MEMBER
diff_hunk: @@ -113,17 +113,43 @@ pub type StringValue = String;
 /// The JSON representation for `BytesValue` is JSON string.
 pub type BytesValue = bytes::Bytes;
 
+pub(crate) struct ProtobufTypeSerializer<T> {
+    _phantom: std::marker::PhantomData<T>,
+}
+
+impl<T> ProtobufTypeSerializer<T> {
+    pub fn new() -> Self {
+        Self {
+            _phantom: std::marker::PhantomData,
+        }
+    }
+}
+
+impl<T> crate::message::MessageSerializer<T> for ProtobufTypeSerializer<T>
path: src/wkt/src/wrappers.rs
user: login: dbolduc
body: This one can also be a `crate::message::ValueSerializer<T>`
created_at: 2025-04-28T16:16:10Z
author_association: MEMBER
diff_hunk: @@ -157,10 +183,23 @@ impl crate::message::Message for UInt64Value {
     fn typename() -> &'static str {
         "type.googleapis.com/google.protobuf.UInt64Value"
     }
-    fn to_map(&self) -> Result<crate::message::Map, crate::AnyError> {
-        encode_string::<Self>(self.to_string())
+
+    fn serializer() -> Box<dyn crate::message::MessageSerializer<Self>>
+    where
+        Self: Sized,
+    {
+        Box::new(UInt64ValueSerializer)
+    }
+}
+
+struct UInt64ValueSerializer;
path: src/wkt/src/wrappers.rs
user: login: dbolduc
body: comment: This needs to stay custom, because it has custom `to_map`, `from_map` implementations.

Same goes for `Int64Value`, `BytesValue`. They also stay custom.
created_at: 2025-04-28T16:17:06Z
author_association: MEMBER
diff_hunk: @@ -20,27 +20,52 @@ use crate::AnyError as Error;
 /// A trait that must be implemented by all messages.
 ///
 /// Messages sent to and received from Google Cloud services may be wrapped in
-/// [Any][crate::any::Any]. `Any` uses a `@type` field to encoding the type
+/// [Any][crate::any::Any]. `Any` uses a `@type` field to encode the type
 /// name and then validates extraction and insertion against this type.
 pub trait Message {
     /// The typename of this message.
     fn typename() -> &'static str;
 
+    /// Returns the serializer for this message type
     #[doc(hidden)]
-    /// Store the value into a JSON object.
-    fn to_map(&self) -> Result<Map, Error>
+    fn serializer() -> Box<dyn MessageSerializer<Self>>
     where
-        Self: serde::ser::Serialize + Sized,
+        Self: Sized + serde::ser::Serialize + serde::de::DeserializeOwned + 'static,
     {
-        to_json_object(self)
+        Box::new(DefaultSerializer::<Self>::new())
     }
+}
 
-    #[doc(hidden)]
-    /// Extract the value from a JSON object.
-    fn from_map(map: &Map) -> Result<Self, Error>
-    where
-        Self: serde::de::DeserializeOwned,
-    {
+/// Internal API for message serialization.
+/// This is not intended for direct use by consumers of this crate.
+#[doc(hidden)]
+pub trait MessageSerializer<T> {
+    fn to_map(&self, message: &T) -> Result<Map, Error>;
+    fn from_map(&self, map: &Map) -> Result<T, Error>;
path: src/wkt/src/message.rs
user: login: dbolduc
body: `clippy` does not like `from_*` when something accepts a `&self`. That is valid, but we are using `&self` and concrete serializer objects to workaround the fact that we can't default types in a trait.

So maybe we change these function names to `serialize(...)`, `deserialize(...)` ?
created_at: 2025-04-28T16:19:48Z
author_association: MEMBER
diff_hunk: @@ -20,27 +20,52 @@ use crate::AnyError as Error;
 /// A trait that must be implemented by all messages.
 ///
 /// Messages sent to and received from Google Cloud services may be wrapped in
-/// [Any][crate::any::Any]. `Any` uses a `@type` field to encoding the type
+/// [Any][crate::any::Any]. `Any` uses a `@type` field to encode the type
 /// name and then validates extraction and insertion against this type.
 pub trait Message {
     /// The typename of this message.
     fn typename() -> &'static str;
 
+    /// Returns the serializer for this message type
     #[doc(hidden)]
-    /// Store the value into a JSON object.
-    fn to_map(&self) -> Result<Map, Error>
+    fn serializer() -> Box<dyn MessageSerializer<Self>>
     where
-        Self: serde::ser::Serialize + Sized,
+        Self: Sized + serde::ser::Serialize + serde::de::DeserializeOwned + 'static,
     {
-        to_json_object(self)
+        Box::new(DefaultSerializer::<Self>::new())
     }
+}
 
-    #[doc(hidden)]
-    /// Extract the value from a JSON object.
-    fn from_map(map: &Map) -> Result<Self, Error>
-    where
-        Self: serde::de::DeserializeOwned,
-    {
+/// Internal API for message serialization.
+/// This is not intended for direct use by consumers of this crate.
+#[doc(hidden)]
+pub trait MessageSerializer<T> {
+    fn to_map(&self, message: &T) -> Result<Map, Error>;
+    fn from_map(&self, map: &Map) -> Result<T, Error>;
path: src/wkt/src/message.rs
user: login: chrischiedo
body: > So maybe we change these function names to `serialize(...)`, `deserialize(...)` ?

Okay. I think that makes sense.
created_at: 2025-04-28T18:56:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,27 +20,52 @@ use crate::AnyError as Error;
 /// A trait that must be implemented by all messages.
 ///
 /// Messages sent to and received from Google Cloud services may be wrapped in
-/// [Any][crate::any::Any]. `Any` uses a `@type` field to encoding the type
+/// [Any][crate::any::Any]. `Any` uses a `@type` field to encode the type
 /// name and then validates extraction and insertion against this type.
 pub trait Message {
     /// The typename of this message.
     fn typename() -> &'static str;
 
+    /// Returns the serializer for this message type
     #[doc(hidden)]
path: src/wkt/src/message.rs
user: login: chrischiedo
body: Noted.
created_at: 2025-04-28T18:57:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,27 +20,52 @@ use crate::AnyError as Error;
 /// A trait that must be implemented by all messages.
 ///
 /// Messages sent to and received from Google Cloud services may be wrapped in
-/// [Any][crate::any::Any]. `Any` uses a `@type` field to encoding the type
+/// [Any][crate::any::Any]. `Any` uses a `@type` field to encode the type
 /// name and then validates extraction and insertion against this type.
 pub trait Message {
     /// The typename of this message.
     fn typename() -> &'static str;
 
+    /// Returns the serializer for this message type
     #[doc(hidden)]
-    /// Store the value into a JSON object.
-    fn to_map(&self) -> Result<Map, Error>
+    fn serializer() -> Box<dyn MessageSerializer<Self>>
path: src/wkt/src/message.rs
user: login: chrischiedo
body: Reverted back to static dispatch.
created_at: 2025-04-30T08:31:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,27 +20,52 @@ use crate::AnyError as Error;
 /// A trait that must be implemented by all messages.
 ///
 /// Messages sent to and received from Google Cloud services may be wrapped in
-/// [Any][crate::any::Any]. `Any` uses a `@type` field to encoding the type
+/// [Any][crate::any::Any]. `Any` uses a `@type` field to encode the type
 /// name and then validates extraction and insertion against this type.
 pub trait Message {
     /// The typename of this message.
     fn typename() -> &'static str;
 
+    /// Returns the serializer for this message type
     #[doc(hidden)]
-    /// Store the value into a JSON object.
-    fn to_map(&self) -> Result<Map, Error>
+    fn serializer() -> Box<dyn MessageSerializer<Self>>
     where
-        Self: serde::ser::Serialize + Sized,
+        Self: Sized + serde::ser::Serialize + serde::de::DeserializeOwned + 'static,
     {
-        to_json_object(self)
+        Box::new(DefaultSerializer::<Self>::new())
     }
+}
 
-    #[doc(hidden)]
-    /// Extract the value from a JSON object.
-    fn from_map(map: &Map) -> Result<Self, Error>
-    where
-        Self: serde::de::DeserializeOwned,
-    {
+/// Internal API for message serialization.
+/// This is not intended for direct use by consumers of this crate.
+#[doc(hidden)]
+pub trait MessageSerializer<T> {
path: src/wkt/src/message.rs
user: login: chrischiedo
body: Done.
created_at: 2025-04-30T08:33:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,27 +20,52 @@ use crate::AnyError as Error;
 /// A trait that must be implemented by all messages.
 ///
 /// Messages sent to and received from Google Cloud services may be wrapped in
-/// [Any][crate::any::Any]. `Any` uses a `@type` field to encoding the type
+/// [Any][crate::any::Any]. `Any` uses a `@type` field to encode the type
 /// name and then validates extraction and insertion against this type.
 pub trait Message {
     /// The typename of this message.
     fn typename() -> &'static str;
 
+    /// Returns the serializer for this message type
     #[doc(hidden)]
-    /// Store the value into a JSON object.
-    fn to_map(&self) -> Result<Map, Error>
+    fn serializer() -> Box<dyn MessageSerializer<Self>>
     where
-        Self: serde::ser::Serialize + Sized,
+        Self: Sized + serde::ser::Serialize + serde::de::DeserializeOwned + 'static,
     {
-        to_json_object(self)
+        Box::new(DefaultSerializer::<Self>::new())
     }
+}
 
-    #[doc(hidden)]
-    /// Extract the value from a JSON object.
-    fn from_map(map: &Map) -> Result<Self, Error>
-    where
-        Self: serde::de::DeserializeOwned,
-    {
+/// Internal API for message serialization.
+/// This is not intended for direct use by consumers of this crate.
+#[doc(hidden)]
+pub trait MessageSerializer<T> {
+    fn to_map(&self, message: &T) -> Result<Map, Error>;
+    fn from_map(&self, map: &Map) -> Result<T, Error>;
+}
+
+// Default serializer that most types can use
+pub(crate) struct DefaultSerializer<T> {
+    _phantom: std::marker::PhantomData<T>,
+}
+
+impl<T> DefaultSerializer<T> {
+    pub fn new() -> Self {
+        Self {
+            _phantom: std::marker::PhantomData,
+        }
+    }
+}
+
+impl<T> MessageSerializer<T> for DefaultSerializer<T>
path: src/wkt/src/message.rs
user: login: chrischiedo
body: Done.
created_at: 2025-04-30T08:33:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -136,10 +149,24 @@ impl crate::message::Message for Any {
     fn typename() -> &'static str {
         "type.googleapis.com/google.protobuf.Any"
     }
-    fn to_map(&self) -> Result<crate::message::Map, AnyError> {
-        crate::message::to_json_other(self)
+
+    // Override the default serializer to use custom serialization
+    fn serializer() -> Box<dyn crate::message::MessageSerializer<Self>>
+    where
+        Self: Sized,
+    {
+        Box::new(AnySerializer)
     }
path: src/wkt/src/any.rs
user: login: chrischiedo
body: Done.
created_at: 2025-04-30T08:33:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -164,10 +164,24 @@ impl crate::message::Message for Duration {
     fn typename() -> &'static str {
         "type.googleapis.com/google.protobuf.Duration"
     }
-    fn to_map(&self) -> Result<crate::message::Map, crate::AnyError> {
-        crate::message::to_json_other(self)
+
+    // Override the default serializer to use custom serialization
+    fn serializer() -> Box<dyn crate::message::MessageSerializer<Self>>
+    where
+        Self: Sized,
+    {
+        Box::new(DurationSerializer)
+    }
+}
+
+struct DurationSerializer;
path: src/wkt/src/duration.rs
user: login: chrischiedo
body: Done.
created_at: 2025-04-30T08:34:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -113,17 +113,43 @@ pub type StringValue = String;
 /// The JSON representation for `BytesValue` is JSON string.
 pub type BytesValue = bytes::Bytes;
 
+pub(crate) struct ProtobufTypeSerializer<T> {
+    _phantom: std::marker::PhantomData<T>,
+}
+
+impl<T> ProtobufTypeSerializer<T> {
+    pub fn new() -> Self {
+        Self {
+            _phantom: std::marker::PhantomData,
+        }
+    }
+}
+
+impl<T> crate::message::MessageSerializer<T> for ProtobufTypeSerializer<T>
path: src/wkt/src/wrappers.rs
user: login: chrischiedo
body: Indeed. Done.
created_at: 2025-04-30T08:34:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -157,10 +183,23 @@ impl crate::message::Message for UInt64Value {
     fn typename() -> &'static str {
         "type.googleapis.com/google.protobuf.UInt64Value"
     }
-    fn to_map(&self) -> Result<crate::message::Map, crate::AnyError> {
-        encode_string::<Self>(self.to_string())
+
+    fn serializer() -> Box<dyn crate::message::MessageSerializer<Self>>
+    where
+        Self: Sized,
+    {
+        Box::new(UInt64ValueSerializer)
+    }
+}
+
+struct UInt64ValueSerializer;
path: src/wkt/src/wrappers.rs
user: login: chrischiedo
body: Yes. No changes have been made to these.
created_at: 2025-04-30T08:35:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -136,10 +149,24 @@ impl crate::message::Message for Any {
     fn typename() -> &'static str {
         "type.googleapis.com/google.protobuf.Any"
     }
-    fn to_map(&self) -> Result<crate::message::Map, AnyError> {
-        crate::message::to_json_other(self)
+
+    // Override the default serializer to use custom serialization
+    fn serializer() -> Box<dyn crate::message::MessageSerializer<Self>>
+    where
+        Self: Sized,
+    {
+        Box::new(AnySerializer)
     }
-    fn from_map(map: &crate::message::Map) -> Result<Self, AnyError> {
+}
+
+struct AnySerializer;
path: src/wkt/src/any.rs
user: login: chrischiedo
body: Done.
created_at: 2025-04-30T08:35:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,27 +20,52 @@ use crate::AnyError as Error;
 /// A trait that must be implemented by all messages.
 ///
 /// Messages sent to and received from Google Cloud services may be wrapped in
-/// [Any][crate::any::Any]. `Any` uses a `@type` field to encoding the type
+/// [Any][crate::any::Any]. `Any` uses a `@type` field to encode the type
 /// name and then validates extraction and insertion against this type.
 pub trait Message {
     /// The typename of this message.
     fn typename() -> &'static str;
 
+    /// Returns the serializer for this message type
     #[doc(hidden)]
-    /// Store the value into a JSON object.
-    fn to_map(&self) -> Result<Map, Error>
+    fn serializer() -> Box<dyn MessageSerializer<Self>>
     where
-        Self: serde::ser::Serialize + Sized,
+        Self: Sized + serde::ser::Serialize + serde::de::DeserializeOwned + 'static,
     {
-        to_json_object(self)
+        Box::new(DefaultSerializer::<Self>::new())
     }
+}
 
-    #[doc(hidden)]
-    /// Extract the value from a JSON object.
-    fn from_map(map: &Map) -> Result<Self, Error>
-    where
-        Self: serde::de::DeserializeOwned,
-    {
+/// Internal API for message serialization.
+/// This is not intended for direct use by consumers of this crate.
+#[doc(hidden)]
+pub trait MessageSerializer<T> {
+    fn to_map(&self, message: &T) -> Result<Map, Error>;
+    fn from_map(&self, map: &Map) -> Result<T, Error>;
path: src/wkt/src/message.rs
user: login: chrischiedo
body: I have gone with `serialize_to_map` and `deserialize_from_map` respectively.
created_at: 2025-04-30T08:37:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,27 +20,52 @@ use crate::AnyError as Error;
 /// A trait that must be implemented by all messages.
 ///
 /// Messages sent to and received from Google Cloud services may be wrapped in
-/// [Any][crate::any::Any]. `Any` uses a `@type` field to encoding the type
+/// [Any][crate::any::Any]. `Any` uses a `@type` field to encode the type
 /// name and then validates extraction and insertion against this type.
 pub trait Message {
     /// The typename of this message.
     fn typename() -> &'static str;
 
+    /// Returns the serializer for this message type
     #[doc(hidden)]
path: src/wkt/src/message.rs
user: login: chrischiedo
body: Done.
created_at: 2025-04-30T08:37:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,8 +24,8 @@ pub(crate) fn handle_start<R, M>(
     result: Result<Operation<R, M>>,
 ) -> (Option<String>, PollingResult<R, M>)
 where
-    R: wkt::message::Message + serde::de::DeserializeOwned,
-    M: wkt::message::Message + serde::de::DeserializeOwned,
+    R: wkt::message::Message + serde::ser::Serialize + serde::de::DeserializeOwned + 'static,
+    M: wkt::message::Message + serde::ser::Serialize + serde::de::DeserializeOwned + 'static,
path: src/lro/src/details.rs
user: login: chrischiedo
body: For now: The only change I've made here is to remove the static lifetime bound, now that we've reverted back to static dispatch for our `fn serializer()` in the `Message` trait. We can always raise another PR if we think separating `MessageSerializer` would be _more_ beneficial.
created_at: 2025-04-30T08:56:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,31 +20,85 @@ use crate::AnyError as Error;
 /// A trait that must be implemented by all messages.
 ///
 /// Messages sent to and received from Google Cloud services may be wrapped in
-/// [Any][crate::any::Any]. `Any` uses a `@type` field to encoding the type
+/// [Any][crate::any::Any]. `Any` uses a `@type` field to encode the type
 /// name and then validates extraction and insertion against this type.
 pub trait Message {
     /// The typename of this message.
     fn typename() -> &'static str;
 
+    /// Returns the serializer for this message type
path: src/wkt/src/message.rs
user: login: coryan
body: nit:  our documentation comments end with a period. Please use the same style in the internal comments too:

```suggestion
    /// Returns the serializer for this message type.
```
created_at: 2025-04-30T12:42:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -164,11 +164,11 @@ impl crate::message::Message for Duration {
     fn typename() -> &'static str {
         "type.googleapis.com/google.protobuf.Duration"
     }
-    fn to_map(&self) -> Result<crate::message::Map, crate::AnyError> {
-        crate::message::to_json_other(self)
-    }
-    fn from_map(map: &crate::message::Map) -> Result<Self, crate::AnyError> {
-        crate::message::from_other(map)
+
+    // Override the default serializer to use custom serialization
path: src/wkt/src/duration.rs
user: login: coryan
body: Consider just removing these internal-only comments. I think the intent is pretty clear.  If you decide to keep them, please also end them with a period:

```suggestion
    // Override the default serializer to use custom serialization.
```
created_at: 2025-04-30T12:44:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,31 +20,85 @@ use crate::AnyError as Error;
 /// A trait that must be implemented by all messages.
 ///
 /// Messages sent to and received from Google Cloud services may be wrapped in
-/// [Any][crate::any::Any]. `Any` uses a `@type` field to encoding the type
+/// [Any][crate::any::Any]. `Any` uses a `@type` field to encode the type
 /// name and then validates extraction and insertion against this type.
 pub trait Message {
     /// The typename of this message.
     fn typename() -> &'static str;
 
+    /// Returns the serializer for this message type
path: src/wkt/src/message.rs
user: login: chrischiedo
body: Done.
created_at: 2025-04-30T15:44:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -164,11 +164,11 @@ impl crate::message::Message for Duration {
     fn typename() -> &'static str {
         "type.googleapis.com/google.protobuf.Duration"
     }
-    fn to_map(&self) -> Result<crate::message::Map, crate::AnyError> {
-        crate::message::to_json_other(self)
-    }
-    fn from_map(map: &crate::message::Map) -> Result<Self, crate::AnyError> {
-        crate::message::from_other(map)
+
+    // Override the default serializer to use custom serialization
path: src/wkt/src/duration.rs
user: login: chrischiedo
body: Decided to remove the internal-only comments.
created_at: 2025-04-30T15:44:52Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-04-28T16:34:23Z
body: ## [Codecov](https://app.codecov.io/gh/googleapis/google-cloud-rust/pull/1946?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=googleapis) Report
Attention: Patch coverage is `94.73684%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 62.11%. Comparing base [(`fd9aa87`)](https://app.codecov.io/gh/googleapis/google-cloud-rust/commit/fd9aa87310054df77b3c57f6cc5229bb2f658674?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=googleapis) to head [(`b17be37`)](https://app.codecov.io/gh/googleapis/google-cloud-rust/commit/b17be37c68a3e735f4986107573bb9bb787360de?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=googleapis).
> Report is 21 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/googleapis/google-cloud-rust/pull/1946?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=googleapis) | Patch % | Lines |
|---|---|---|
| [src/wkt/src/message.rs](https://app.codecov.io/gh/googleapis/google-cloud-rust/pull/1946?src=pr&el=tree&filepath=src%2Fwkt%2Fsrc%2Fmessage.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=googleapis#diff-c3JjL3drdC9zcmMvbWVzc2FnZS5ycw==) | 81.81% | [2 Missing :warning: ](https://app.codecov.io/gh/googleapis/google-cloud-rust/pull/1946?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=googleapis) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##             main    #1946       +/-   ##
===========================================
- Coverage   96.32%   62.11%   -34.22%     
===========================================
  Files          56       64        +8     
  Lines        2042     3223     +1181     
===========================================
+ Hits         1967     2002       +35     
- Misses         75     1221     +1146     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/googleapis/google-cloud-rust/pull/1946?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=googleapis).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=googleapis).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: chrischiedo
created_at: 2025-04-28T18:55:36Z
body: > Thanks! We are getting closer...

@dbolduc Thanks for the quick review! Much appreciated. Let me work on effecting the suggested changes.
author_association: CONTRIBUTOR
user: login: chrischiedo
created_at: 2025-04-30T09:00:50Z
body: Here's the update PR with the review changes.

cc: @dbolduc 
author_association: CONTRIBUTOR
user: login: dbolduc
created_at: 2025-04-30T11:39:58Z
body: /gcbrun
author_association: MEMBER
user: login: coryan
created_at: 2025-04-30T15:46:50Z
body: /gcbrun
author_association: CONTRIBUTOR
user: login: chrischiedo
created_at: 2025-04-30T15:52:41Z
body: > This came together really nicely. I appreciate the collaboration on design + all the implementation work you did, @chrischiedo. Thanks a bunch.

Thank YOU! This was a GREAT learning opportunity for me. Thanks a lot for allowing me to work on it. Really appreciated, @dbolduc.
author_association: CONTRIBUTOR