I have a struct which contains a Result field that I want to serialize
#[derive(Serialize, Deserialize)]
struct ServerResponse<D,E> {
result : Result<D,E>,
version : String,
// More parameters here
}
I want to be able to serialize and deserialize this struct to the following-format.
If result is successful
{
"result" : "my_result_data",
"version" : "1.0",
}
If the result is not successful
{
"error" : { "message" : "The request failed", "error-code" : 212 },
"version" : "1.0"
}
I have one issue with your question: You want Err
to be deserialized from a json object with the fields message
and error-code
, yet it's a generic parameter and there's no indication that it is constrained to any specific type.
I've chosen to work around that issue by defining a new trait:
pub trait ResponseError {
fn message(&self) -> &str;
fn error_code(&self) -> i32;
fn from_parts(message: &str, code: i32) -> Self;
}
Any ServerResponse
that wants to be serialized must E: ResponseError
. That might entirely be the wrong thing to do, but with what limited information I have, it's the best thing I can do.
The actual implementation is quite mechanic:
Set up ServerResponse
to delegate de/serialization to a set of custom functions:
#[derive(Serialize, Deserialize, Debug)]
#[serde(bound(
serialize = "D: Serialize, E: ResponseError",
deserialize = "D: DeserializeOwned, E: ResponseError"
))]
struct ServerResponse<D, E> {
#[serde(with = "untagged_ok_result")]
result: Result<D, E>,
version: String,
// More parameters here
}
Scaffold said functions
mod untagged_ok_result {
pub fn serialize<'a, S, T, E>(v: &Result<T, E>, ser: S) -> Result<S::Ok, S::Error> { todo!() }
pub fn deserialize<'de, D, T, E>(de: D) -> Result<Result<T, E>, D::Error> { todo!() }
}
Define a Rust type where derived De/Serialize matches whatever actual JSON format you want
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
#[serde(bound(deserialize = "D: Deserialize<'a>"))]
enum Mresult<'a, D> {
Ok(D),
Err {
#[serde(borrow)]
message: Cow<'a, str>,
#[serde(rename = "error-code")]
error_code: i32,
},
}
Implement de/serialization by converting between the std
Result
and the custom type:
pub fn serialize<'a, S, T, E>(v: &Result<T, E>, ser: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
T: Serialize,
E: ResponseError,
{
match v {
Ok(v) => Mresult::Ok(v),
Err(v) => Mresult::Err {
message: v.message().into(),
error_code: v.error_code(),
},
}
.serialize(ser)
}
pub fn deserialize<'de, D, T, E>(de: D) -> Result<Result<T, E>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de>,
E: ResponseError,
{
Ok(match Mresult::deserialize(de)? {
Mresult::Ok(v) => Ok(v),
Mresult::Err {
message,
error_code,
} => Err(ResponseError::from_parts(&message, error_code)),
})
}
(I was tempted to use the from
/into
attributes for a while, but those are container attributes, which causes other boilerplate. It might be an option for you anyway)