如何使用 serde 自定义结果类型的序列化?

huangapple go评论69阅读模式
英文:

How customize serialization of a result-type using serde?

问题

#[derive(Serialize, Deserialize)]
struct ServerResponse<D, E> {
   result: Result<D, E>,
   version: String,
   // More parameters here
}

我想要能够将这个结构序列化和反序列化为以下格式。

如果结果成功

{
   "result": "my_result_data",
   "version": "1.0"
}

如果结果不成功

{
   "error": { "message": "请求失败", "error-code": 212 },
   "version": "1.0"
}
英文:

I have a struct which contains a Result field that I want to serialize

#[derive(Serialize, Deserialize)]
struct ServerResponse&lt;D,E&gt; {
   result : Result&lt;D,E&gt;,
   version : String,
   // More parameters here
}

I want to be able to serialize and deserialize this struct to the following-format.

If result is successful

{
   &quot;result&quot; : &quot;my_result_data&quot;,
   &quot;version&quot; : &quot;1.0&quot;,
}

If the result is not successful

{
   &quot;error&quot; : { &quot;message&quot; : &quot;The request failed&quot;, &quot;error-code&quot; : 212 },
   &quot;version&quot; : &quot;1.0&quot;
}

答案1

得分: 1

我对你的问题有一个疑问:你希望从一个包含 messageerror-code 字段的 JSON 对象中反序列化 Err,然而它是一个泛型参数,并且没有指示它受到任何特定类型的约束。

我选择通过定义一个新的 trait 来解决这个问题:

pub trait ResponseError {
    fn message(&self) -> &str;
    fn error_code(&self) -> i32;
    fn from_parts(message: &str, code: i32) -> Self;
}

任何想要被序列化的 ServerResponse 必须满足 E: ResponseError。这可能完全是错误的做法,但根据我所了解的有限信息,这是我能做的最好的事情。

实际的实现相当机械:

  1. 设置 ServerResponse 委托序列化和反序列化给一组自定义函数:
#[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,
    // 更多参数在这里
}
  1. 搭建这些函数的框架:
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!() }
}
  1. 定义一个 Rust 类型,其中派生的 De/Serialize 匹配实际的 JSON 格式:
#[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,
    },
}
  1. 通过在 stdResult 和自定义类型之间进行转换来实现序列化和反序列化:
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 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(&amp;self) -&gt; &amp;str;
    fn error_code(&amp;self) -&gt; i32;
    fn from_parts(message: &amp;str, code: i32) -&gt; 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:

  1. Set up ServerResponse to delegate de/serialization to a set of custom functions:

    #[derive(Serialize, Deserialize, Debug)]
    #[serde(bound(
        serialize = &quot;D: Serialize, E: ResponseError&quot;,
        deserialize = &quot;D: DeserializeOwned, E: ResponseError&quot;
    ))]
    struct ServerResponse&lt;D, E&gt; {
        #[serde(with = &quot;untagged_ok_result&quot;)]
        result: Result&lt;D, E&gt;,
        version: String,
        // More parameters here
    }
    
  2. Scaffold said functions

    mod untagged_ok_result {
        pub fn serialize&lt;&#39;a, S, T, E&gt;(v: &amp;Result&lt;T, E&gt;, ser: S) -&gt; Result&lt;S::Ok, S::Error&gt; { todo!() }
        pub fn deserialize&lt;&#39;de, D, T, E&gt;(de: D) -&gt; Result&lt;Result&lt;T, E&gt;, D::Error&gt; { todo!() }
    }
    
  3. Define a Rust type where derived De/Serialize matches whatever actual JSON format you want

    #[derive(Serialize, Deserialize)]
    #[serde(untagged)]
    #[serde(bound(deserialize = &quot;D: Deserialize&lt;&#39;a&gt;&quot;))]
    enum Mresult&lt;&#39;a, D&gt; {
        Ok(D),
        Err {
            #[serde(borrow)]
            message: Cow&lt;&#39;a, str&gt;,
            #[serde(rename = &quot;error-code&quot;)]
            error_code: i32,
        },
     }
    
  4. Implement de/serialization by converting between the std Result and the custom type:

    pub fn serialize&lt;&#39;a, S, T, E&gt;(v: &amp;Result&lt;T, E&gt;, ser: S) -&gt; Result&lt;S::Ok, S::Error&gt;
    where
        S: Serializer,
        T: Serialize,
        E: ResponseError,
    {
        match v {
            Ok(v) =&gt; Mresult::Ok(v),
            Err(v) =&gt; Mresult::Err {
                message: v.message().into(),
                error_code: v.error_code(),
            },
        }
        .serialize(ser)
    }
    pub fn deserialize&lt;&#39;de, D, T, E&gt;(de: D) -&gt; Result&lt;Result&lt;T, E&gt;, D::Error&gt;
    where
        D: Deserializer&lt;&#39;de&gt;,
        T: Deserialize&lt;&#39;de&gt;,
        E: ResponseError,
    {
        Ok(match Mresult::deserialize(de)? {
            Mresult::Ok(v) =&gt; Ok(v),
            Mresult::Err {
                message,
                error_code,
            } =&gt; Err(ResponseError::from_parts(&amp;message, error_code)),
        })
    }
    

Playground

(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)

huangapple
  • 本文由 发表于 2023年7月3日 20:21:19
  • 转载请务必保留本文链接:https://go.coder-hub.com/76604700.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定