rustjson-deserializationserde

Deserialize escaped string into Vec<String> with serde


Newbie in Rust here. I'm having some difficulties parsing a escaped json string into a Option<Vec<String>>.

I have an api that's returning a collection of items, and one of the properties contains an array of strings, however, the array is escaped as a string. Here an example, please check the keywords property.

        {
            "id": "123123132",
            "properties": {
                "createdate": "2023-06-25T03:10:43.312Z",
                "description": "Lorem Impsum ......",
                "keywords": "[\"keyword1\", \"keyword2\", \"keyword3\"]"
            },
            "createdAt": "2023-06-25T03:10:43.312Z",
            "updatedAt": "2024-04-03T14:40:20.360Z",
        },
        {
            "id": "789789789",
            "properties": {
                "createdate": "2023-06-25T03:10:43.312Z",
                "description": "Another description ......",
                "keywords": null
            },
            "createdAt": "2023-06-25T03:10:43.312Z",
            "updatedAt": "2024-04-03T14:40:20.360Z",
        },

Sadly, I do not have any control over the API, so I was trying to parse it as a string array. Also, that property is not mandatory, so, sometimes can contain null values.

I have tried implementing the following code in Rust:


fn deserialize_keywords<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
    where
        D: serde::Deserializer<'de>,
{
    let s: &str = de::Deserialize::deserialize(deserializer)?;
    match serde_json::from_str(s) {
        Result::Ok(v) => Result::Ok(Some(v)),
        Result::Err(e) => Result::Ok(None)
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ItemProperties {
    id: String,
    description: Option<String>,

    #[serde(deserialize_with = "deserialize_keywords")]
    keywords: Option<Vec<String>>,

    #[serde(alias = "createdate")]
    created_date: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Item {
    pub id: String,
    pub properties: ItemProperties,
    #[serde(alias = "createdAt")]
    pub created_at: String,
    #[serde(alias = "updatedAt")]
    pub updated_at: String,
}



#[derive(Deserialize)]
struct TypeResponse {
    results: Vec<Item>,
    total: usize,
}

let response = self.client.post("https://api.com/items")
            .send()
            .await?
            .json::<TypeResponse>()
            .await?;

However, when I try that I have the following error:

Error: error decoding response body: invalid type: null, expected a borrowed string at line 1 column 349

Caused by:
    invalid type: null, expected a borrowed string at line 1 column 349

I actually tried a few more things, but nothing has worked so far. Any idea how can I achieve it?


Solution

  • As BalllpointBen already mentioned, you cannot deserialize a JSON string with escape sequences into &str so you have to replace it with String and since it may not be there (null is possible) you'll have to deserialize into an Option<String>:

    use serde::{de, Deserialize};
    fn deserialize_keywords<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let Some(keywords_string): Option<String> = de::Deserialize::deserialize(deserializer)? else {
            return Ok(vec![]);
        };
    
        let Ok(keywords) = serde_json::from_str(&keywords_string) else {
            return Ok(vec![]);
        };
    
        Ok(keywords)
    }
    
    // here follows a more minimal example I used to test.
    const JSON: &str =
        r#"[{ "keywords": "[\"keyword1\", \"keyword2\", \"keyword3\"]"}, {"keywords": null}]"#;
    
    fn main() {
        let foos: Vec<Foo> = serde_json::from_str(JSON).unwrap();
        dbg!(foos);
    }
    
    
    #[derive(Debug, Deserialize)]
    struct Foo {
        #[serde(deserialize_with = "deserialize_keywords")]
        keywords: Vec<String>,
    }
    

    I've additionally removed the ambiguous representation of no keywords (Some(vec![]) and None) since that's unlikely what you want.