Search code examples
rustserde

Custom serde serialization for enum type


I have the following data structure which should be able to hold either a String, a u64 value, a boolean value, or a String vector.

#[derive(Serialize, Deserialize)]
pub enum JsonRpcParam {
    String(String),
    Int(u64),
    Bool(bool),
    Vec(Vec<String>)
}

The use case for this data structure is to build JSON RPC parameters which can have multiple types, so I would be able to build a parameter list like this:

let mut params : Vec<JsonRpcParam> = Vec::new();
params.push(JsonRpcParam::String("Test".to_string()));
params.push(JsonRpcParam::Bool(true));
params.push(JsonRpcParam::Int(64));
params.push(JsonRpcParam::Vec(vec![String::from("abc"), String::from("cde")]));

My problem is now the serialization. I am using serde_json for the serialization part. The default serialization of the vector posted above yields:

[
   {
      "String":"Test"
   },
   {
      "Bool":true
   },
   {
      "Int":64
   },
   {
      "Vec":[
         "abc",
         "cde"
      ]
   }
]

Instead, I would like the serialization to look like this:

[
   "Test",
   true,
   64,
   ["abc","cde"]
]

I attempted to implement a custom serialize method for this type, but dont't know how to achieve what I want, my attempt looks like this:

impl Serialize for JsonRpcParam {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer {
        match *self {
            JsonRpcParam::String(x) => serializer.serialize_str(x),
            JsonRpcParam::Int(x) => serializer.serialize_u64(x),
            JsonRpcParam::Bool(x) => serializer.serialize_bool(x),
            JsonRpcParam::Vec(x) => _
        }
    }
}

Solution

  • Instead of manually implementing Serialize you can instead use #[serde(untagged)].

    In your case that will work perfectly fine. However, be warned that if the enum variant isn't unique and can't be clearly identified from the JSON, then it will deserialize into the first variant that matches. In short if you also have e.g. a subsequent JsonRpcParam::OtherString(String), then that will deserialize into JsonRpcParam::String(String).

    #[derive(Serialize, Deserialize, Debug)]
    #[serde(untagged)]
    pub enum JsonRpcParam {
        String(String),
        Int(u64),
        Bool(bool),
        Vec(Vec<String>),
    }
    

    If you now use e.g. serde_json::to_string_pretty(), then it will yield an output in your desired format:

    fn main() {
        let mut params: Vec<JsonRpcParam> = Vec::new();
        params.push(JsonRpcParam::String("Test".to_string()));
        params.push(JsonRpcParam::Bool(true));
        params.push(JsonRpcParam::Int(64));
        params.push(JsonRpcParam::Vec(vec![
            String::from("abc"),
            String::from("cde"),
        ]));
    
        let json = serde_json::to_string_pretty(&params).unwrap();
        println!("{}", json);
    }
    

    Output:

    [
      "Test", 
      true,   
      64,     
      [       
        "abc",
        "cde" 
      ]       
    ]