The serde crate is the standard for serializing and deserializing data in Rust. It allows you to convert Rust structs to JSON and back using powerful macros. Combined with serde_json, it gives you type-safe and flexible control over reading and writing structured data.
I first used serde when building a CLI tool that needed to read a configuration file in JSON. I was amazed at how easy it was to map JSON into strongly typed Rust structs. It helped catch mistakes early, and I never had to worry about missing or invalid fields at runtime.
In this post, you will learn how to serialize Rust types to JSON, deserialize JSON into Rust, and customize the process with optional fields, custom keys, and nested data.
Add serde and serde_json to Your Project
In your Cargo.toml:
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
These crates work together: serde handles the logic, and serde_json handles JSON parsing specifically.
Basic Deserialization: JSON to Rust
You can turn a JSON string into a Rust struct:
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct User {
name: String,
age: u8,
}
fn main() {
let data = r#"{"name": "Alice", "age": 30}"#;
let user: User = serde_json::from_str(data).unwrap();
println!("{:?}", user);
}
This parses the JSON and fills the fields based on name and type. If any field is missing or the type is wrong, you get an error.
Serialization: Rust to JSON
You can turn a struct into a JSON string:
use serde::Serialize;
#[derive(Serialize)]
struct Product {
name: String,
price: f64,
}
fn main() {
let product = Product {
name: "Book".into(),
price: 12.99,
};
let json = serde_json::to_string(&product).unwrap();
println!("{}", json);
}
Output:
{"name":"Book","price":12.99}
Use to_string_pretty for readable formatting.
Optional Fields
You can use Option<T> for optional values:
#[derive(Deserialize)]
struct Config {
theme: Option<String>,
}
This field can be missing in the JSON and will default to None.
Renaming Fields
Use #[serde(rename = “…”)] if the JSON key does not match your field name:
#[derive(Deserialize)]
struct Record {
#[serde(rename = "user_id")]
id: u32,
}
This allows you to keep idiomatic Rust field names and still match external formats.
Ignoring Fields
Use #[serde(skip_serializing)] or #[serde(skip_deserializing)] to ignore fields during conversion:
#[derive(Serialize, Deserialize)]
struct Secret {
name: String,
#[serde(skip_serializing)]
password: String,
}
This keeps sensitive or internal data out of the JSON output.
Nested Structs
Serde works with nested structures:
#[derive(Serialize, Deserialize)]
struct Post {
title: String,
author: User,
}
#[derive(Serialize, Deserialize)]
struct User {
name: String,
}
You can map entire JSON trees into nested Rust structs.
Reading JSON from Files
Use fs::read_to_string and from_str:
use std::fs;
use serde::deserialize;
#[derive(Deserialize)]
struct Config {
debug: bool,
}
fn main() {
let data = fs::read_to_string("config.json").unwrap();
let config: Config = serde_json::from_str(&data).unwrap();
println!("Debug: {}", config.debug);
}
This is perfect for reading settings or local data.
Writing JSON to Files
Use serde_json::to_string_pretty and fs::write:
use serde::Serialize;
use std::fs;
#[derive(Serialize)]
struct Log {
event: String,
success: bool,
}
fn main() {
let log = Log {
event: "Login".into(),
success: true,
};
let json = serde_json::to_string_pretty(&log).unwrap();
fs::write("log.json", json).unwrap();
}
This gives you clean, human-readable JSON files.
Summary
The serde crate makes it safe and easy to work with JSON in Rust. You can read config files, interact with APIs, or persist structured data, all while keeping strong type guarantees. With support for optional fields, custom keys, and full control, it is one of Rust’s most mature and widely used tools.
Rust Intermediate Concepts
- Generic Types in Functions and Structs
- Implementing Traits for Custom Behavior in Rust
- Trait Bounds and Constraints in Rust
- Lifetimes in Rust
- Using Closures (Anonymous Functions) in Rust
- The Iterator Trait and .next() in Rust
- Higher Order Iterator Methods: map, filter, and fold in Rust
- Using impl Trait for Simplicity in Rust
- Advanced Collections: HashSet and BTreeMap in Rust
- Custom Error Types and the Error Trait in Rust
- Option and Result Combinators in Rust
- Writing Unit Tests in Rust
- Integration Testing in Rust
- Logging in Rust with the log Crate
- Cargo Tips and Tricks for Rust Projects
- CLI Argument Parsing with Clap in Rust
- File I/O and File System Operations in Rust
- Running External Commands in Rust
- Make HTTP Requests in Rust with Reqwest
- JSON Serialization and Deserialization in Rust with Serde
- Building a Weather CLI in Rust
- Date and Time Handling in Rust with Chrono
- Using Regular Expressions in Rust with the regex Crate
- Memory Management in Rust
- Understanding Borrow Checker Errors in Rust
- Interacting with Databases in Rust
- Building a Todo List CLI in Rust with SQLite
- Concurrency in Rust