Sitemap
faizal khan

faizal khan

Rust developer , hire me at morsebits.com or contact at Faizalkhn98641@gmail.com

Rust serde 101

6 min readMay 11, 2024

--

If you’re a Rust developer working in the web domain or any other field that necessitates serialization or deserialization to convert data formats into Rust native data types, it’s highly likely that you’ve used serde at least once or more. Serde, being a powerful and widely-used serialization framework in the Rust ecosystem, simplifies the process of seamlessly converting data between different representations. Whether you’re dealing with JSON, XML, or other formats, serde provides a flexible and customizable solution, enabling you to focus on your application logic rather than low-level data handling intricacies. If you haven’t explored serde yet, it’s a tool well worth adding to your Rust toolkit for handling data interchange with ease and precision.
In this blog I am going to tell you some useful serde derive macros that can really help you .

Press enter or click to view image in full size

The most simplest way to implement Serialize and Deserialize trait on Structs is using derive macro ie.
we will be discussing Human object for simplicity and will be using json data format for our examples

#[derive(Serialize, Deserialize)]
pub struct Human{
human_age:i32,
human_name:String,
human_location:String,
}

Now consider this human.json file

{
humanName: "rust" ,
HumanAge: 12 ,
humanLocation: "India , Delhi "
}

As evident, the majority of keys in the JSON files follow the camelCase convention. While it’s not mandatory for a JSON file, it’s a commonly adopted practice. On the other hand your rust Human struct keys must have snake case or else your compiler will give you tons of warnings .

quick tip — if you ever feel like these rust struct naming
convention are not for you and you’re tired of compiler warning, you can always use #[allow(dead_code] on top of your struct to remove warnings… there are other macros as well which can help you reduce warning these linting errors

serde rename

Now then we need to deserialize data from the given human.json file, but the keys is mismatched in our rust native struct and json file, how we’re going to deal with it? #[serde(rename(deserialize = "<variable name to deserialize from>"))], ie we can use this field attribute to deserialize keys even if the names are mismatched ,

#[derive(Serialize, Deserialize)]
pub struct Human{
#[serde(rename(deserialize = "humanAge"))]
human_age:i32,
#[serde(rename(deserialize = "humanName"))]
human_name:String,
#[serde(rename(deserialize = "humanLocation"))]
human_location:String,

}

remember, we can also serialize keys into another name using

#[serde(rename(serialize = "<variable name to deserialize from>"))]

While the idea sounds promising, what happens if one or more essential keys are absent from the JSON file? When developing APIs, we’re often mindful that certain fields might be optional in the JSON body. However, if we fail to provide the complete JSON body, will the code still compile? Unfortunately, it won’t .

serde default

Lets consider in our example what if the humanAge key is missing sometimes and provided in some, we can use Option enum for this.

#[derive(Serialize, Deserialize)]
pub struct Human{
#[serde(rename(deserialize = "humanAge"))]
human_age:Option<i32>,
#[serde(rename(deserialize = "humanName"))]
human_name:String,
#[serde(rename(deserialize = "humanLocation"))]
human_location:String,

}

In this scenario, when the humanAge key is present in the JSON, human_age will store Option::Some(age) in its content. However, if you prefer your field to be optional ( from frontend perspective ) and want to assign a default value when not provided by an external resource, you can make the necessary adjustments in the code.

Get faizal khan’s stories in your inbox

Join Medium for free to get updates from this writer.

You can always have a default value from your keys if data is missing during Serialize or Deserialize process .
note the data that is being Serialize should impl default trait , or you would get a compile time error.
but what if you don’t want the dafault value of something, for example in case of Option<String> , if the value is missing , you rather want Some(“default name”) than None? well for this we have a default function to call when the value is missing

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
pub struct Human {
#[serde(rename(deserialize = "humanAge"))]
human_age: Option<i32>,
#[serde(rename(deserialize = "humanName"), default = "default_human_name")]
human_name: Option<String>,
#[serde(rename(deserialize = "humanLocation"))]
human_location: String,
}

// Function that provides a default name
fn default_human_name() -> Option<String> {
Some("Faizal Khan".to_string())
}


#[cfg(test)]
mod tests {
use super::*;
use serde_json;

#[test]
fn test_deserialize_human() {
// JSON input simulating a response from an API or external source.
let json_data = r#"
{
"humanAge": 30,
"humanLocation": "Mumbai"
}
"#;

// Deserialize json_data into a Human struct.
let deserialized_human: Human = serde_json::from_str(json_data).unwrap();

// Check if the human_age is correctly deserialized.
assert_eq!(deserialized_human.human_age, Some(30));
// Check if the human_name uses the default value as the field is not provided in JSON.
assert_eq!(deserialized_human.human_name, Some("Faizal Khan".to_string()));
// Check if the human_location is correctly deserialized.
assert_eq!(deserialized_human.human_location, "Mumbai");
}
}

Serde Untagged

As a backend developer specializing in REST APIs, you frequently deal with situations where the same function needs to process different types of requests from the frontend. These requests can vary, primarily in the JSON body they carry, depending on the context of the request. In scenarios like this, the #[serde(untagged)] attribute in Rust can be particularly useful.

For instance, take a scenario involving a Human struct with fields for age, name, and location. Suppose your authentication function needs to handle both humans and aliens, but validating aliens requires additional fields beyond those used for humans. In such cases, you could define an enum that includes variants for both humans and aliens, utilizing the #[serde(untagged)] attribute. This attribute helps serde to dynamically determine the appropriate enum variant to use for deserialization based on the structure of the incoming data, effectively differentiating between human and alien data without explicitly tagging each JSON payload..

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
pub struct Human {
#[serde(rename(deserialize = "humanAge"))]
human_age: Option<i32>,
#[serde(rename(deserialize = "humanName"), default = "default_human_name")]
human_name: Option<String>,
#[serde(rename(deserialize = "humanLocation"))]
human_location: String,
}

// Function that provides a default name
fn default_human_name() -> Option<String> {
Some("Faizal Khan".to_string())
}

#[derive(Serialize, Deserialize)]
struct Alien{
race : String ,
planet : String,
Galaxy : String ,
planet_id : String,
ship_number: i32
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub Enum AuthUser{
AuthHuman(Human),
AuthAlien(Alien)
}

#[cfg(test]
mod test{
#[test]
fn test_deserialize_alien() {
let json = r#"
{
"race": "Martian",
"planet": "Mars",
"Galaxy": "Milky Way",
"planet_id": "M123",
"ship_number": 42
}
"#;

let deserialized: AuthUser = serde_json::from_str(json).unwrap();

match deserialized {
AuthUser::AuthAlien(alien) => {
assert_eq!(alien.race, "Martian");
assert_eq!(alien.planet, "Mars");
assert_eq!(alien.Galaxy, "Milky Way");
assert_eq!(alien.planet_id, "M123");
assert_eq!(alien.ship_number, 42);
},
_ => panic!("Expected Alien but got Human"),
}
}
}

Custom Serialization

n our example, when serializing data from our Alien struct in Rust, there's a need to include additional fields that aren't originally present in the struct. Implementing custom serialization allows us to seamlessly add these fields to the serialized output. This flexibility is particularly useful not only for including metadata like timestamps or server locations but also for performing computations that can be included directly in the serialized data. For instance, we might calculate the estimated travel time based on the alien's origin and destination, or dynamically determine the status of their mission and include these as fields in the serialization.

Beyond adding fields and calculations, custom serialization can also address several other use cases:

  • Preserving Encapsulation: Safeguard sensitive data by controlling what gets serialized, thus maintaining the integrity and privacy of the data.
  • Interoperability with Different Data Formats: Customize serialization to conform to specific external system requirements or data formats that might not be natively supported by standard serializers.
  • Performance Optimization: Streamline the serialized output to include only what is necessary, reducing the size of the data payload and potentially enhancing the performance of data transmission.
  • Versioning: Manage changes in the data structure over time without breaking older versions of applications by customizing how data is serialized and deserialized.
use serde::ser::{Serialize, Serializer, SerializeStruct};
use std::fmt;

#[derive(Debug)]
struct Alien {
race: String,
planet: String,
galaxy: String,
planet_id: String,
ship_number: i32,
}

impl Serialize for Alien {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let timestamp = "2024-05-11T12:00:00Z"; // Example static value
let location_of_server = "Server-42"; // Example static value
let serve = "Active"; // Example static value

let mut state = serializer.serialize_struct("Alien", 8)?;
state.serialize_field("race", &self.race)?;
state.serialize_field("planet", &self.planet)?;
state.serialize_field("galaxy", &self.galaxy)?;
state.serialize_field("planet_id", &self.planet_id)?;
state.serialize_field("ship_number", &self.ship_number)?;
state.serialize_field("timestamp", &timestamp)?;
state.serialize_field("location_of_server", &location_of_server)?;
state.serialize_field("serve", &serve)?;
state.end()
}
}

Thats it, if you like this article consider hiring me or my team at morsebits.com .

We’re team of professional developers , we really charge very less.
Thanks

--

--