Loading page…
Rust walkthroughs
Loading page…
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:
Serialize trait — convert Rust types to data formatsDeserialize trait — convert data formats to Rust typesserde_json — JSON-specific implementation (most common format)Serde is foundational for APIs, configuration files, and data interchange.
# 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(())
}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(())
}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(())
}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(())
}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(())
}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(())
}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(())
}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(())
}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(())
}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(())
}Serialize and Deserialize with #[derive(Serialize, Deserialize)]serde_json::to_string() and from_str() for JSONto_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{"Variant": {...}} (default)#[serde(tag = "type")] puts variant in a field#[serde(tag = "type", content = "data")]#[serde(untagged)] tries variants in orderserde_json::Value handles arbitrary JSON with json! macropointer("/path/to/field") for nested JSON accessSerialize and Deserialize traits manuallyserde(with = "module") for reusable custom serialization