Sitemap
Level Up Coding

Coding tutorials and news. The developer homepage gitconnected.com && skilled.dev && levelup.dev

Rust: Custom Serde Fields Deserialization

3 min readJul 31, 2024

--

Press enter or click to view image in full size
By ChatGPT, Dalle 3

Serde is an awesome framework for serializing and deserializing Rust data structures. If you write Web App, need to process api responses, you have probably used it at some time in your life.

It really makes serializing and deserializing a lot more efficient!

BUT!

API responses are some time not as awesome as they should be.

Have you encountered the following cases?

  • Having a field that should be a vector of strings, but if only 1 value is presented, the response ends up being a single String instead? Ex: names: “itsuki1” instead of names: [“itsuki1”].
  • Have a numeric field but when the data is not available, instead of making it null or remove the key, you will receive an empty String as its value? Ex: age: “” instead of age: null.

Until whoever design those APIs decided to fix that for us, let’s see how we can use a custom deserialize_with to handle it!

Set Up

Let’s use the following struct as an example where the favorite_food is the problematic field. We normal will have a Vec<String> as our response, but if only 1 value is presented, it will end up being a String instead.


#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct UserInfo {
name: String,
email: String,
favorite_food: Vec<String>
}

Obviously, when the response is a Vec<String>, Deserialize works just like what we expect. And obviously again! It will not work if the response is a String.

    let json = json!({
"name": "itsuki",
"email": "itsuki@example.com",
"favorite_food": ["PB&J", "steak"]
}).to_string();
let result = deserialize(&json);
assert!(result.is_ok());
assert_eq!(result.unwrap(), UserInfo {
name: "itsuki".to_owned(),
email: "itsuki@example.com".to_owned(),
favorite_food: vec!["PB&J".to_owned(), "steak".to_owned()]
});

let json = json!({
"name": "itsuki",
"email": "itsuki@example.com",
"favorite_food": "PB&J"
}).to_string();
let result = deserialize(&json);
assert!(result.is_err());

deserialize is just a little helper function.

fn deserialize(json: &str) -> Result<UserInfo, Box<dyn Error>> {
let result = serde_json::from_str::<UserInfo>(json)?;
return Ok(result)
}

Custom Field deserialization

This can be achieve by marking the field with #[serde(deserialize_with = "path")].

Get Itsuki’s stories in your inbox

Join Medium for free to get updates from this writer.

This will deserialize this field using a function that is different from its implementation of Deserialize. The given function must be callable as fn<'de, D>(D) -> Result<T, D::Error> where D: Deserializer<'de>, although it may also be generic over T. Fields used with deserialize_with are not required to implement Deserialize.

Let’s see how we use that in our example above.

We will first define our function to convert either a Vec<String> or a String to a Vec<String> like following.

fn to_vec_string<'de, D>(deserializer: D)
-> Result<Vec<String>, D::Error> where D: serde::Deserializer<'de> {

#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrVec {
String(String),
Vec(Vec<String>),
}
match StringOrVec::deserialize(deserializer)? {
StringOrVec::String(s) => Ok(vec![s]),
StringOrVec::Vec(v) => Ok(v),
}
}

We can then mark our favorite_food field with #[serde(deserialize_with = “to_vec_string”)].

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct UserInfo {
name: String,
email: String,
#[serde(deserialize_with = "to_vec_string")]
favorite_food: Vec<String>
}

And both examples from above will work out as expected this time.

let json = json!({
"name": "itsuki",
"email": "itsuki@example.com",
"favorite_food": ["PB&J", "steak"]
}).to_string();
let result = deesrialize(&json);
assert!(result.is_ok());
assert_eq!(result.unwrap(), UserInfo {
name: "itsuki".to_owned(),
email: "itsuki@example.com".to_owned(),
favorite_food: vec!["PB&J".to_owned(), "steak".to_owned()]
});

let json = json!({
"name": "itsuki",
"email": "itsuki@example.com",
"favorite_food": "PB&J"
}).to_string();
let result = deesrialize(&json);
assert!(result.is_ok());
assert_eq!(result.unwrap(), UserInfo {
name: "itsuki".to_owned(),
email: "itsuki@example.com".to_owned(),
favorite_food: vec!["PB&J".to_owned()]
});

Of course, you can simply mark the favorite_food field to be of type StringOrVec, but I think this approach is a lot cleaner!

Thank you for reading! That’s all I have for today!

I love PB&J!

Happy deserializing while biting into your sandwich!

--