skip to Main Content

I have the following struct

#[derive(Serialize)]
pub struct MyStruct {
    pub id: String,
    pub score: f32,
    pub json: String,
}

The json field always contains a valid JSON object already stringified.

Given an instance, I would like to serialize it with the JSON content.
Something like:

let a = MyStruct {
    id: "my-id".to_owned(),
    score: 20.3,
    json: r#"{
       "ffo": 4
    }"#,
};
let r = to_string(&a).unwrap();
assert_eq!(r, r#"{
        "id": "my-id",
        "score": 20.3,
        "json": {
            "ffo": 4
        }
    }"#);

NB: I don’t need to support different serialization formats, only JSON.
NB2: I’m sure that json field always contains a valid JSON object.
NB3: commonly I use serde but I’m open to using different libraries.

How can I do that?

Edit:
I would like to avoid deserializing the string during the serialization if possible.

2

Answers


  1. You can do it, but you have to override the default serialization behaviour somehow. You can do this either by wrapping your json field in a newtype (like struct JsonString(String) and implementing Serialize manually for that type, or you can use the #[serde(serialize_with = "...")] field attribute to ad-hoc change the serialization of the json field. Here’s an example of using the serialize_with field attribute:

    use serde::{ser::Error, Serialize, Serializer};
    
    use serde_json::Value;
    
    #[derive(Serialize)]
    pub struct MyStruct {
        pub id: String,
        pub score: f32,
        #[serde(serialize_with = "as_json_object")]
        pub json: String,
    }
    
    fn as_json_object<S>(v: &str, s: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let v: Value =
            serde_json::from_str(v).map_err(|_| Error::custom("error parsing serialized json"))?;
    
        v.serialize(s)
    }
    
    fn main() {
        let a = MyStruct {
            id: "my-id".to_owned(),
            score: 20.3,
            json: r#"{
               "ffo": 4
            }"#
            .to_owned(),
        };
    
        let r = serde_json::to_string(&a).unwrap();
    
        assert_eq!(r, r#"{"id":"my-id","score":20.3,"json":{"ffo":4}}"#);
    }
    

    Playground.

    Login or Signup to reply.
  2. serde_json has a raw_value feature for something like this:

    Cargo.toml

    # ...
    [dependencies]
    serde = { version = "1.0", features = ["derive"] }
    serde_json = { version = "1.0", features = ["raw_value"] }
    

    lib.rs

    use serde::{Serializer, Serialize};
    use serde_json::{self, value::RawValue};
    
    #[derive(Serialize)]
    pub struct MyStruct {
        pub id: String,
        pub score: f32,
        #[serde(serialize_with = "serialize_raw_json")]
        pub json: String,
    }
    
    fn serialize_raw_json<S>(json: &str, s: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // This should be pretty efficient: it just checks that the string is valid;
        // it doesn't parse it into a new data structure.
        let v: &RawValue = serde_json::from_str(json).expect("invalid json");
        v.serialize(s)
    }
    
    #[test]
    fn test_serialize() {
        let a = MyStruct {
            id: "my-id".to_owned(),
            score: 20.3,
            json: r#"{
               "ffo": 4
            }"#
            .to_string(),
        };
    
        let r = serde_json::to_string(&a).unwrap();
        assert_eq!(
            r,
            r#"{"id":"my-id","score":20.3,"json":{
               "ffo": 4
            }}"#
        );
    }
    

    But the simplest (and most error-prone and least extensible) solution is simple string manipulation:

    #[derive(Serialize)]
    pub struct MyStruct {
        pub id: String,
        pub score: f32,
        // IMPORTANT: don't serialize this field at all
        #[serde(skip)]
        pub json: String,
    }
    
    fn serialize(a: &MyStruct) -> String {
        let mut r = serde_json::to_string(&a).unwrap();
    
        // get rid of trailing '}'
        r.pop();
        // push the key
        r.push_str(r#","json":"#);
        // push the value
        r.push_str(&a.json);
        // push the closing brace
        r.push('}');
        
        r
    }
    
    #[test]
    fn test_serialize() {
        let a = MyStruct {
            id: "my-id".to_owned(),
            score: 20.3,
            json: r#"{
               "ffo": 4
            }"#
            .to_string(),
        };
    
        let r = serialize(&a);
        assert_eq!(
            r,
            r#"{"id":"my-id","score":20.3,"json":{
               "ffo": 4
            }}"#
        );
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search