How do I serialize and deserialize data in Rust?

Walkthrough

Serde (Serialize/Deserialize) is the standard framework for converting Rust types to and from formats like JSON, TOML, YAML, and more. It uses procedural macros (#[derive(Serialize, Deserialize)]) to generate efficient serialization code at compile time. Serde is zero-cost for many operations and handles complex types, generics, and lifetimes elegantly.

Key concepts:

  1. Serialize trait — convert Rust types to data formats
  2. Deserialize trait — convert data formats to Rust types
  3. serde_json — JSON-specific implementation (most common format)
  4. Attributes — customize field names, skip fields, handle variants
  5. Transcoders — convert between formats without intermediate types

Serde is foundational for APIs, configuration files, and data interchange.

Code Example

# Cargo.toml
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
use serde::{Deserialize, Serialize};
 
#[derive(Debug, Serialize, Deserialize)]
struct User {
    id: u32,
    name: String,
    email: String,
    active: bool,
}
 
fn main() -> serde_json::Result<()> {
    // Create a user
    let user = User {
        id: 1,
        name: "Alice".to_string(),
        email: "alice@example.com".to_string(),
        active: true,
    };
    
    // Serialize to JSON
    let json = serde_json::to_string(&user)?;
    println!("JSON: {}", json);
    
    // Deserialize from JSON
    let parsed: User = serde_json::from_str(&json)?;
    println!("Parsed: {:?}", parsed);
    
    Ok(())
}

Pretty Printing and Formatted JSON

use serde::{Deserialize, Serialize};
 
#[derive(Debug, Serialize, Deserialize)]
struct Config {
    name: String,
    version: String,
    settings: Settings,
}
 
#[derive(Debug, Serialize, Deserialize)]
struct Settings {
    debug: bool,
    timeout: u32,
    hosts: Vec<String>,
}
 
fn main() -> serde_json::Result<()> {
    let config = Config {
        name: "myapp".to_string(),
        version: "1.0.0".to_string(),
        settings: Settings {
            debug: true,
            timeout: 30,
            hosts: vec!["localhost".to_string(), "127.0.0.1".to_string()],
        },
    };
    
    // Compact JSON
    let compact = serde_json::to_string(&config)?;
    println!("Compact: {}", compact);
    
    // Pretty-printed JSON
    let pretty = serde_json::to_string_pretty(&config)?;
    println!("Pretty:\n{}", pretty);
    
    // Write to a string with indentation
    let mut output = String::new();
    let formatter = serde_json::ser::PrettyFormatter::with_indent(b"    ");
    let mut serializer = serde_json::Serializer::with_formatter(&mut output, formatter);
    config.serialize(&mut serializer)?;
    println!("Custom indent:\n{}", output);
    
    Ok(())
}

Renaming Fields and Variants

use serde::{Deserialize, Serialize};
 
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ApiResponse {
    status_code: u32,
    error_message: Option<String>,
    data: Vec<Item>,
}
 
#[derive(Debug, Serialize, Deserialize)]
struct Item {
    #[serde(rename = "itemId")]
    item_id: String,
    #[serde(rename = "displayName")]
    display_name: String,
    quantity: u32,
}
 
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
enum Status {
    NotStarted,
    InProgress,
    Completed,
    Failed,
}
 
fn main() -> serde_json::Result<()> {
    let response = ApiResponse {
        status_code: 200,
        error_message: None,
        data: vec![
            Item {
                item_id: "abc123".to_string(),
                display_name: "Widget".to_string(),
                quantity: 5,
            },
        ],
    };
    
    let json = serde_json::to_string_pretty(&response)?;
    println!("{}", json);
    
    // Result:
    // {
    //   "statusCode": 200,
    //   "errorMessage": null,
    //   "data": [
    //     {
    //       "itemId": "abc123",
    //       "displayName": "Widget",
    //       "quantity": 5
    //     }
    //   ]
    // }
    
    let status = Status::InProgress;
    println!("Status: {}", serde_json::to_string(&status)?);
    // Output: "IN_PROGRESS"
    
    Ok(())
}

Optional Fields and Defaults

use serde::{Deserialize, Serialize};
 
#[derive(Debug, Serialize, Deserialize)]
struct Config {
    #[serde(default)]
    name: String,
    
    #[serde(default = "default_timeout")]
    timeout: u32,
    
    #[serde(default, skip_serializing_if = "Option::is_none")]
    description: Option<String>,
    
    #[serde(skip_serializing_if = "Vec::is_empty")]
    tags: Vec<String>,
    
    #[serde(default, skip_serializing_if = "is_false")]
    debug: bool,
}
 
fn default_timeout() -> u32 { 30 }
fn is_false(b: &bool) -> bool { !b }
 
fn main() -> serde_json::Result<()> {
    // Partial JSON - missing fields get defaults
    let json = r#"{"name": "test"}"#;
    let config: Config = serde_json::from_str(json)?;
    println!("Parsed: {:?}", config);
    
    // Serialize - empty/skipped fields omitted
    let config = Config {
        name: "test".to_string(),
        timeout: 30,
        description: None,
        tags: vec![],
        debug: false,
    };
    println!("Serialized: {}", serde_json::to_string(&config)?);
    // Output: {"name":"test","timeout":30}
    
    // With all fields populated
    let full = Config {
        name: "full".to_string(),
        timeout: 60,
        description: Some("A description".to_string()),
        tags: vec!["tag1".to_string()],
        debug: true,
    };
    println!("Full: {}", serde_json::to_string(&full)?);
    
    Ok(())
}

Handling Enums and Variants

use serde::{Deserialize, Serialize};
 
// Unit variants
#[derive(Debug, Serialize, Deserialize)]
enum Color {
    Red,
    Green,
    Blue,
}
 
// Externally tagged (default)
#[derive(Debug, Serialize, Deserialize)]
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
}
 
// Internally tagged
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
event Event {
    Click { x: i32, y: i32 },
    KeyPress { key: String },
}
 
// Adjacently tagged
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
enum Payload {
    Text(String),
    Number(i32),
}
 
// Untagged - tries each variant in order
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
enum Value {
    Float(f64),
    Integer(i64),
    String(String),
}
 
fn main() -> serde_json::Result<()> {
    // Unit variant
    let color = Color::Red;
    println!("Color: {}", serde_json::to_string(&color)?);
    // Output: "Red"
    
    // Externally tagged
    let msg = Message::Move { x: 10, y: 20 };
    println!("Message: {}", serde_json::to_string(&msg)?);
    // Output: {"Move":{"x":10,"y":20}}
    
    // Internally tagged
    let event = Event::Click { x: 100, y: 200 };
    println!("Event: {}", serde_json::to_string(&event)?);
    // Output: {"type":"Click","x":100,"y":200}
    
    // Adjacently tagged
    let payload = Payload::Text("hello".to_string());
    println!("Payload: {}", serde_json::to_string(&payload)?);
    // Output: {"type":"Text","data":"hello"}
    
    // Untagged
    let values = vec![
        Value::Float(3.14),
        Value::Integer(42),
        Value::String("text".to_string()),
    ];
    for v in values {
        let json = serde_json::to_string(&v)?;
        let parsed: Value = serde_json::from_str(&json)?;
        println!("Value: {} -> {:?}", json, parsed);
    }
    
    Ok(())
}

Collections and Maps

use serde::{Deserialize, Serialize};
use std::collections::{HashMap, BTreeMap, HashSet};
 
#[derive(Debug, Serialize, Deserialize)]
struct Database {
    users: HashMap<String, User>,
    #[serde(serialize_with = "ordered_map")]
    settings: BTreeMap<String, String>,
    tags: HashSet<String>,
}
 
#[derive(Debug, Serialize, Deserialize)]
struct User {
    name: String,
    email: String,
}
 
fn main() -> serde_json::Result<()> {
    // HashMap -> JSON object
    let mut users = HashMap::new();
    users.insert(
        "user1".to_string(),
        User {
            name: "Alice".to_string(),
            email: "alice@example.com".to_string(),
        },
    );
    users.insert(
        "user2".to_string(),
        User {
            name: "Bob".to_string(),
            email: "bob@example.com".to_string(),
        },
    );
    
    let json = serde_json::to_string_pretty(&users)?;
    println!("Users:\n{}", json);
    
    // Parse JSON object into HashMap
    let json = r#"{"a": 1, "b": 2, "c": 3}"#;
    let map: HashMap<String, i32> = serde_json::from_str(json)?;
    println!("Map: {:?}", map);
    
    // JSON array to Vec
    let json = r#"[1, 2, 3, 4, 5]"#;
    let vec: Vec<i32> = serde_json::from_str(json)?;
    println!("Vec: {:?}", vec);
    
    // HashSet
    let tags: HashSet<String> = serde_json::from_str(r#"["rust", "serde", "json"]"#)?;
    println!("Tags: {:?}", tags);
    
    Ok(())
}

Custom Serialization

use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
 
// Custom serialize/deserialize for Duration
#[derive(Debug)]
struct Duration(u64);
 
impl Serialize for Duration {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(&format!("{}ms", self.0))
    }
}
 
impl<'de> Deserialize<'de> for Duration {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        let num: u64 = s.trim_end_matches("ms").parse().map_err(serde::de::Error::custom)?;
        Ok(Duration(num))
    }
}
 
// Using serde_with module for custom serialization
#[derive(Debug, Serialize, Deserialize)]
struct Task {
    name: String,
    #[serde(with = "duration_ms")]
    duration: Duration,
}
 
mod duration_ms {
    use super::Duration;
    use serde::{Deserialize, Deserializer, Serialize, Serializer};
    
    pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_u64(duration.0)
    }
    
    pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
    where
        D: Deserializer<'de>,
    {
        let ms = u64::deserialize(deserializer)?;
        Ok(Duration(ms))
    }
}
 
fn main() -> serde_json::Result<()> {
    let task = Task {
        name: "Process data".to_string(),
        duration: Duration(5000),
    };
    
    let json = serde_json::to_string(&task)?;
    println!("Task: {}", json);
    
    let parsed: Task = serde_json::from_str(&json)?;
    println!("Parsed: {:?}", parsed);
    
    Ok(())
}

Flatten and From/Into

use serde::{Deserialize, Serialize};
 
#[derive(Debug, Serialize, Deserialize)]
struct Pagination {
    page: u32,
    per_page: u32,
    total: u32,
}
 
#[derive(Debug, Serialize, Deserialize)]
struct ApiResponse {
    data: Vec<String>,
    #[serde(flatten)]
    pagination: Pagination,
}
 
#[derive(Debug, Serialize, Deserialize)]
struct BaseConfig {
    name: String,
    version: String,
}
 
#[derive(Debug, Serialize, Deserialize)]
struct FullConfig {
    #[serde(flatten)]
    base: BaseConfig,
    debug: bool,
}
 
fn main() -> serde_json::Result<()> {
    // Flatten - pagination fields appear at top level
    let response = ApiResponse {
        data: vec!["item1".to_string(), "item2".to_string()],
        pagination: Pagination {
            page: 1,
            per_page: 10,
            total: 100,
        },
    };
    
    let json = serde_json::to_string_pretty(&response)?;
    println!("{}", json);
    // Output:
    // {
    //   "data": ["item1", "item2"],
    //   "page": 1,
    //   "per_page": 10,
    //   "total": 100
    // }
    
    // Parse flattened structure
    let json = r#"{"name":"test","version":"1.0","debug":true}"#;
    let config: FullConfig = serde_json::from_str(json)?;
    println!("Config: {:?}", config);
    
    Ok(())
}

Handling Unknown Fields and Variants

use serde::{Deserialize, Serialize};
 
// Ignore unknown fields (default behavior)
#[derive(Debug, Serialize, Deserialize)]
struct StrictUser {
    name: String,
    email: String,
}
 
// Capture unknown fields
#[derive(Debug, Serialize, Deserialize)]
struct FlexibleUser {
    name: String,
    email: String,
    #[serde(flatten)]
    extra: serde_json::Value,
}
 
// Handle unknown enum variants
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
enum FlexibleStatus {
    Known(Status),
    Unknown(String),
}
 
#[derive(Debug, Serialize, Deserialize)]
enum Status {
    Active,
    Inactive,
    Pending,
}
 
fn main() -> serde_json::Result<()> {
    // Unknown fields are ignored
    let json = r#"{"name":"Alice","email":"alice@test.com","age":30}"#;
    let user: StrictUser = serde_json::from_str(json)?;
    println!("Strict: {:?}", user);
    
    // Capture unknown fields
    let json = r#"{"name":"Alice","email":"alice@test.com","age":30,"role":"admin"}"#;
    let flex: FlexibleUser = serde_json::from_str(json)?;
    println!("Flexible: {:?}", flex);
    println!("Extra: {}", flex.extra);
    
    // Handle unknown variants
    let json = r#""unknown_status""#;
    let status: FlexibleStatus = serde_json::from_str(json)?;
    println!("Status: {:?}", status);
    
    Ok(())
}

Working with serde_json::Value

use serde_json::{json, Value};
 
fn main() -> serde_json::Result<()> {
    // Create JSON dynamically
    let value = json!({
        "name": "Alice",
        "age": 30,
        "skills": ["rust", "python", "go"],
        "address": {
            "city": "Boston",
            "country": "USA"
        }
    });
    
    println!("JSON: {}", serde_json::to_string_pretty(&value)?);
    
    // Access values
    if let Some(name) = value.get("name").and_then(|v| v.as_str()) {
        println!("Name: {}", name);
    }
    
    if let Some(skills) = value.get("skills").and_then(|v| v.as_array()) {
        println!("Skills: {:?}", skills);
    }
    
    // Pointer syntax for nested access
    if let Some(city) = value.pointer("/address/city").and_then(|v| v.as_str()) {
        println!("City: {}", city);
    }
    
    // Modify JSON
    let mut data = json!({"count": 0});
    data["count"] = json!(42);
    data["label"] = json!("items");
    println!("Modified: {}", data);
    
    // Parse arbitrary JSON
    let json_str = r#"{"array":[1,2,3],"null":null}"#;
    let parsed: Value = serde_json::from_str(json_str)?;
    println!("Parsed: {:?}", parsed);
    
    Ok(())
}

Summary

  • Derive Serialize and Deserialize with #[derive(Serialize, Deserialize)]
  • Use serde_json::to_string() and from_str() for JSON
  • Use to_string_pretty() for formatted output
  • #[serde(rename = "...")] renames individual fields
  • #[serde(rename_all = "camelCase")] renames all fields automatically
  • #[serde(default)] uses Default for missing fields
  • #[serde(skip_serializing_if = "...")] omits fields conditionally
  • #[serde(flatten)] embeds struct fields at the parent level
  • Externally tagged enums: {"Variant": {...}} (default)
  • Internally tagged: #[serde(tag = "type")] puts variant in a field
  • Adjacently tagged: #[serde(tag = "type", content = "data")]
  • Untagged: #[serde(untagged)] tries variants in order
  • serde_json::Value handles arbitrary JSON with json! macro
  • Use pointer("/path/to/field") for nested JSON access
  • Custom serialization: implement Serialize and Deserialize traits manually
  • Use serde(with = "module") for reusable custom serialization