Rust Tonic `serde::Serialize`'ing types including Google's WKTs
I’m increasing using Rust in addition to Golang to write code. tonic is really excellent but I’d been struggling with coupling its generated types with serde_json because, by default, tonic doesn’t generate types with serde::Serialize annotations.
For what follows, the Protobuf sources are in Google’s googleapis repo, specifically google.firestore.v1.
(JSON) serializing generated types
For example, I’d like to do the following:
use google::firestore::v1::ListenRequest;
async fn mmain() -> Result<(), Box<dyn std::error::Error>> {
...
let rqst = ListenRequest { ... };
let json_string = serde_json::to_string(&rqst)?;
dbg!(json_string);
}
Yields:
the trait bound `ListenRequest: serde::ser::Serialize` is not satisfied
Because:
google.firestore.v1.rs:
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ListenRequest { ... }
But, by adding the following type_attribute to the Builder:
build.rs:
tonic_build::configure()
.type_attribute(
// The Protobuf package prefix is optional
"google.firestore.v1.ListenRequest",
"#[derive(serde::Serialize)]",
)
.compile_protos(...)?;
The generated code is revised:
google.firestore.v1.rs:
#[derive(serde::Serialize)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ListenRequest { ... }
Unfortunately, in this case, the problem isn’t solved because ListenRequest comprises child types that must also be serialized.
The simplest solution is to add serde::Serialize annotations to everything:
tonic_build::configure()
.type_attribute(
// Everything
".",
"#[derive(serde::Serialize)]",
)
.compile_protos(...)?;
The alternative appears (!) to be to recursively descend through the subset of types needed and annotate them.
One curiosity from doing this is how to represent nested types.
In the case of ListenRequest, after adding the type_attribute("google.firestore.v1.ListenRequest", ...), one of these nested types, errors:
the trait `Serialize` is not implemented for `listen_request::TargetChange`
Because:
google.firestore.v1.rs:
#[derive(serde::Serialize)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ListenRequest {
...
#[prost(oneof = "listen_request::TargetChange", tags = "2, 3")]
pub target_change: ::core::option::Option<listen_request::TargetChange>,
}
Here’s the trick! Even though the type is listen_request::TargetChange, the type_attribute annotation must reflect the Protobuf source path and field name google.firestore.v1.ListenRequest.target_change:
build.rs:
tonic_build::configure()
.type_attribute(
"google.firestore.v1.ListenRequest",
"#[derive(serde::Serialize)]",
)
.type_attribute(
"google.firestore.v1.ListenRequest.target_change"),
"#[derive(serde::Serialize)]",
)
.compile_protos(...)?;
Protocol Buffers Well-Known Types
While we’re on the subject, there’s a different mechanism for ensuring that Protocol Buffers Well-known Types can be serialized using the prost-wkt-types crate:
build.rs:
tonic_build::configure()
.extern_path(
format!(".google.protobuf.Any"),
"::prost_wkt_types::Any"),
.extern_path(
format!(".google.protobuf.Timestamp"),
"::prost_wkt_types::Timestamp",
)
.compile_protos(...)?;
NOTE Not only does this use
extern_pathbut the fully-qualified name must be prefixed with.