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
163: Deriving Serialize and Deserialize
At some point in every Rust project, you're going to need to move your data out of the program's memory and into a format that can be saved to a disk or sent over a network. Usually, that means JSON, TOML, or BSON. In the Rust ecosystem, serde is the gold standard for this. But when you first look at the Serialize and Deserialize traits, you might be tempted to implement them manually to keep "total control" over the process.
The Manual Slog of Trait Implementation
Imagine we have a PlayerProfile struct for a game. It has a username, a level, and a list of equipped item IDs. If you decided to implement Serialize manually, you'd have to write a serialize method that explicitly tells the serializer how to handle every single field. You'd be calling serializer.serialize_struct, then manually mapping every field name to its value, and finally closing the struct.
// I'm not suggesting you actually do this, but this is what the "manual" path looks like
impl Serialize for PlayerProfile {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer {
let mut state = serializer.serialize_struct("PlayerProfile", 3)?;
state.serialize_field("username", &self.username)?;
state.serialize_field("level", &self.level)?;
state.serialize_field("items", &self.items)?;
state.end()
}
}
This is a maintenance disaster. Every time you add a new field to your PlayerProfile—say, a guild_id—your code will still compile, but your saved files will simply be missing that data because you forgot to update the manual implementation. It's a silent failure that will haunt you in production. And don't even get me started on implementing Deserialize by hand; you'd have to write a visitor pattern that manually parses every key and value. It's tedious, verbose, and frankly, a waste of your time.
Letting the Macro Do the Heavy Lifting
The better way—and the way you'll see in almost every professional Rust codebase—is to use #[derive(Serialize, Deserialize)]. By adding these attributes, you're telling the Rust compiler to use a procedural macro to generate all that boilerplate for you at compile time.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
pub struct PlayerProfile {
pub username: String,
pub level: u32,
pub items: Vec<u32>,
}
Now, if you add guild_id to the struct, the macro automatically updates the serialization logic. There's no risk of forgetting a field, and the code is significantly cleaner. I’ve found that the only real "cost" here is a slight increase in compile time because the macro has to run, but the trade-off in developer sanity is well worth it.
When the Default isn't Enough
You might worry that by using derive, you've lost that "total control" I mentioned earlier. But Serde provides #[serde(...)] attributes that let you tweak the behavior without reverting to manual implementation. For example, Rust uses snake_case for fields, but most JSON APIs expect camelCase. Instead of writing a manual serializer to change the keys, you can just annotate the struct.
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlayerProfile {
pub username: String,
pub current_level: u32, // Will be serialized as "currentLevel"
}
I usually rely on these attributes for 99% of my needs. Whether it's renaming a single field with #[serde(rename = "old_name")] or skipping a field entirely with #[serde(skip)], the derive approach remains safer and more readable than writing the traits by hand. Unless you are building a highly non-standard data format, stick to the derive macros.
📋 Practical Task
Implementing a Config Parser for a Game Engine
You are building a configuration system for a game engine. You need to create a GameConfig struct that can be loaded from a JSON file. However, the JSON file you are receiving from the art team uses "Kebab-Case" (e.g., "window-width") instead of Rust's standard snake_case.
Your requirements:
- Create a struct named
GameConfigwith the following fields:window_width(u32),window_height(u32), andfullscreen(bool). - Use
serde's derive macros to enable serialization and deserialization. - Apply a container attribute to ensure the fields are mapped from
kebab-casein JSON tosnake_casein your Rust struct. - Write a small
mainfunction that usesserde_json::from_strto parse the following string into your struct:"{\"window-width\": 1920, \"window-height\": 1080, \"fullscreen\": true}". - Print the resulting struct to the console to verify the mapping worked.
There are no comments for now.