In our digital age, data is the lifeblood of countless applications. But there’s a tiny secret behind the scenes: the art and science of data conversion. Think about it — how does data smoothly transition from complex structures to a neat, shareable format and back? Enter the world of serialisation.
If you’re working with Rust, one hero in this realm stands head and shoulders above the rest: Serde. Not only is it powerful, but it’s also remarkably friendly to use. So, whether you’re looking to simplify data storage, facilitate data transfer, or are just curious about Rust’s capabilities, join us as we explore data serialisation with Serde.
Ready to dive in? Let’s go! 🚀
What is Serialization?
Serialisation converts complex data structures, such as structs, enums, and other composite types, into a flat, byte-based representation. The reverse process, called deserialisation, involves converting that byte-based representation to its original structure.
Why Serde?
There are several reasons why Serde stands out in Rust’s ecosystem:
- Performance: Serde is known for its high-speed serialisation and deserialisation.
- Flexibility: It supports numerous data formats, including JSON, TOML, YAML, and Binary.
- Customizability: Serde allows developers to define their types’ custom serialisation and deserialisation logic.
Getting Started with Serde
To start using Serde, you need to include the Serde library and its dependencies in your Cargo.toml:
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
In this example, we’ll focus on JSON serialisation.
Serialising and Deserialising with Serde
Here’s a basic example of serialising and deserialising a Rust struct with Serde:
extern crate serde;
extern crate serde_json;
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct Person {
name: String,
age: u8,
}
fn main() {
let person = Person { name: "Alice".to_string(), age: 30 };
// Serialize to JSON string
let serialized = serde_json::to_string(&person).unwrap();
println!("Serialized: {}", serialized);
// Deserialize from JSON string
let deserialized: Person = serde_json::from_str(&serialized).unwrap();
println!("Deserialized: {:?}", deserialized);
}
Implementing Custom Data Types
Imagine you want to store a date in the format dd-MM-yyyy. Rust's standard library doesn't use this format by default, so you need a custom implementation:
use serde::{Serialize, Deserialize, Serializer, Deserializer};
use chrono::{NaiveDate, Datelike, FormatError};
#[derive(Debug)]
struct CustomDate(NaiveDate);
impl Serialize for CustomDate {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let formatted = format!("{:02}-{:02}-{}", self.0.day(), self.0.month(), self.0.year());
serializer.serialize_str(&formatted)
}
}
impl<'de> Deserialize<'de> for CustomDate {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
NaiveDate::parse_from_str(&s, "%d-%m-%Y").map_err(serde::de::Error::custom).map(CustomDate)
}
}
Working with External Structs
Sometimes, you might want to serialise or deserialise types from external crates that don’t implement Serialize and Deserialize. With Serde, you can use newtype pattern:
use std::net::IpAddr;
#[derive(Serialize, Deserialize)]
struct SerializableIpAddr(#[serde(with = "ip_addr_as_string")] IpAddr);
mod ip_addr_as_string {
use std::net::IpAddr;
use serde::{Serialize, Deserialize, Serializer, Deserializer};
pub fn serialize<S>(addr: &IpAddr, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&addr.to_string())
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<IpAddr, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
Conditionally Skip Fields
You might want to skip serialising certain fields based on their values in certain scenarios. Serde allows conditional checks for this:
#[derive(Serialize, Deserialize, Debug)]
struct Product {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
}
In this example, if description is None, it will be skipped during serialisation.
Use of Enums to Represent Variants
Enums in Rust can be used effectively to represent various data variants, especially when working with JSON objects that could have different structures:
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
enum Event {
Login { username: String, timestamp: u64 },
Logout { user_id: u32, timestamp: u64 },
}
This will serialise to either:
{
"type": "Login",
"username": "Alice",
"timestamp": 1633287843
}
Or:
{
"type": "Logout",
"user_id": 42,
"timestamp": 1633287843
}


