#Serialization

Serde is Rust's standard framework for serializing and deserializing data. It supports JSON, TOML, YAML, MessagePack, and dozens of other formats through a unified interface.

#Deriving Serialize and Deserialize

Add serde to your Cargo.toml and derive the traits:

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct User {
    name: String,
    email: String,
    age: u32,
    #[serde(default)]
    admin: bool,
}

#Working with JSON

use serde::{Deserialize, Serialize};
use serde_json;

#[derive(Debug, Serialize, Deserialize)]
struct Config {
    database_url: String,
    port: u16,
    debug: bool,
}

fn main() -> Result<(), serde_json::Error> {
    let config = Config {
        database_url: "postgres://localhost/mydb".to_string(),
        port: 5432,
        debug: true,
    };

    let json = serde_json::to_string_pretty(&config)?;
    println!("{json}");

    let parsed: Config = serde_json::from_str(&json)?;
    println!("{parsed:?}");

    Ok(())
}

#Working with TOML

TOML is popular for configuration files:

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct AppConfig {
    title: String,
    #[serde(default = "default_port")]
    port: u16,
    database: DatabaseConfig,
}

#[derive(Debug, Serialize, Deserialize)]
struct DatabaseConfig {
    url: String,
    max_connections: u32,
}

fn default_port() -> u16 {
    8080
}

fn load_config(path: &str) -> Result<AppConfig, Box<dyn std::error::Error>> {
    let contents = std::fs::read_to_string(path)?;
    let config: AppConfig = toml::from_str(&contents)?;
    Ok(config)
}

#Serde Attributes

Control serialization behavior with attributes:

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ApiResponse {
    status_code: u16,
    error_message: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    retry_after: Option<u64>,

    #[serde(rename = "type")]
    response_type: String,

    #[serde(flatten)]
    metadata: std::collections::HashMap<String, serde_json::Value>,
}

#Enum Serialization

Serde supports multiple enum representations:

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
enum Event {
    #[serde(rename = "user_created")]
    UserCreated { id: u64, name: String },

    #[serde(rename = "order_placed")]
    OrderPlaced { id: u64, total: f64 },

    #[serde(rename = "system_alert")]
    SystemAlert(String),
}

#Custom Serialization

For special cases, implement custom serialization logic:

use serde::{self, Deserialize, Deserializer, Serializer};

fn serialize_uppercase<S>(value: &str, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    serializer.serialize_str(&value.to_uppercase())
}

fn deserialize_lowercase<'de, D>(deserializer: D) -> Result<String, D::Error>
where
    D: Deserializer<'de>,
{
    let value = String::deserialize(deserializer)?;
    Ok(value.to_lowercase())
}

#Summary

Serde's derive macros handle the vast majority of serialization needs. Use attributes to customize field names, skip fields, and control enum representations. Only reach for custom serialization when the built-in options aren't sufficient.