Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
60: Serialization with Serde
I've been working on this small CLI tool to manage a collection of vintage synthesizers, and I hit a wall today. I have a Synth struct that holds all the patch settings, and I realized that every time I restart the app, I lose everything. I need to save this state to a file and load it back. My first instinct was to just format!() the struct into a string, but that's a nightmare to parse back into a Rust object. That's why we're diving into Serde.
The manual struggle
I started by defining my data. Nothing fancy, just a few fields to keep track of the synth's state:
struct SynthPatch {
name: String,
oscillator_freq: f32,
filter_cutoff: f32,
is_active: bool,
}
I thought, "I'll just use the serde_json crate to turn this into a string." I added the dependency and tried this:
let patch = SynthPatch {
name: String::from("Deep Space Bass"),
oscillator_freq: 440.0,
filter_cutoff: 1200.0,
is_active: true,
};
let json = serde_json::to_string(&patch).unwrap();
println!("{}", json);
The compiler immediately yelled at me. It told me that SynthPatch doesn't implement the Serialize trait. This is the core philosophy of Rust: the language doesn't magically know how to traverse your custom structs. It needs an explicit contract. I can't just tell Rust "make this JSON"; I have to tell the type how to be serializable.
Letting Serde handle the heavy lifting
Now, I could implement the Serialize trait by hand, but that's a tedious process involving a lot of boilerplate that I simply don't have the patience for. This is where the derive feature of Serde comes in. I noticed that if I add serde = { version = "1.0", features = ["derive"] } to my Cargo.toml, I can use a procedural macro to do the work for me.
I updated my struct like this:
use serde::Serialize;
#[derive(Serialize)]
struct SynthPatch {
name: String,
oscillator_freq: f32,
filter_cutoff: f32,
is_active: bool,
}
I ran the code again, and it worked. The Serialize derive macro essentially writes the logic to visit every field in my struct and map it to a JSON key. It's seamless. But saving is only half the battle. If I can't get the data back into a SynthPatch instance, the file is just a useless text document.
Bringing the data back to life
I tried to use serde_json::from_str to load a JSON string back into my struct, but I hit the same wall as before. The compiler complained that SynthPatch didn't implement Deserialize. It makes sense—serializing is just reading, but deserializing requires the ability to actually construct the type from a stream of data.
I just added Deserialize to my derive list, and suddenly the "loading" part of my app started working:
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct SynthPatch {
name: String,
oscillator_freq: f32,
filter_cutoff: f32,
is_active: bool,
}
fn main() {
let data = r#"{"name": "Nebula Pad", "oscillator_freq": 220.0, "filter_cutoff": 800.0, "is_active": false}"#;
let decoded: SynthPatch = serde_json::from_str(data).unwrap();
println!("Loaded patch: {:?}", decoded);
}
I love how Rust's type system handles this. Because I told from_str that I expected a SynthPatch (via the type annotation : SynthPatch), Serde knows exactly which deserialization logic to trigger.
Matching JSON keys to Rust fields
Here is where things got annoying. I decided I wanted my JSON files to use snake_case for the keys to follow standard JSON conventions, but I realized some of the external API data I'm pulling in uses camelCase (like oscillatorFreq instead of oscillator_freq).
I didn't want to rename my Rust fields because that would break my naming conventions. I found out that Serde has "field attributes" that let you map names without changing the code. I added a rename_all attribute to the top of the struct:
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct SynthPatch {
name: String,
oscillator_freq: f32, // This will be "oscillatorFreq" in JSON
filter_cutoff: f32, // This will be "filterCutoff" in JSON
is_active: bool, // This will be "isActive" in JSON
}
This is a lifesaver. If I have one specific field that is totally weird—like a legacy key called p_name_val—I can just use #[serde(rename = "p_name_val")] directly on that specific field. Now my Rust code stays clean, and my JSON stays compatible with whatever external system I'm talking to.
📋 Practical Task
Build a Game Character Save-State System
You are building a save-game system for a space exploration RPG. You need to create a system that can save a player's progress to a JSON string and load it back without losing data.
- Create a struct named
PlayerSavewith the following fields:player_name(String)level(u32)coordinates(a tuple of two f64s: x and y)inventory(a Vector of Strings)
- Add the necessary Serde derives so the struct can be both serialized and deserialized.
- Use
#[serde(rename_all = "PascalCase")]on the struct so that the resulting JSON keys start with capital letters (e.g.,"PlayerName"instead of"player_name"). - In your
mainfunction:- Instantiate a
PlayerSaveobject with some sample data. - Serialize it to a JSON string using
serde_json::to_stringand print the string to the console. - Take that printed JSON string, pass it into
serde_json::from_str, and deserialize it back into a newPlayerSaveinstance. - Print the final deserialized object using
{:?}to verify the data survived the round trip.
- Instantiate a
There are no comments for now.