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
165: Serde Attributes: rename, skip, default
I've noticed a recurring pattern when developers first start working with Serde: they treat their Rust structs as direct mirrors of the JSON they're consuming. They'll look at a JSON response from a third-party API, see a field called userId, and immediately name their Rust field userId. Then, they're hit with a barrage of compiler warnings telling them that userId should be user_id to follow Rust's naming conventions. To silence the warnings, they use #[allow(non_snake_case)].
This is a mistake. You should never compromise your Rust code's idiom just to satisfy a poorly named external API. The data format is a contract, but your internal representation is your domain. You can—and should—keep them separate.
Stop naming Rust fields to match JSON keys
Instead of fighting the compiler, we use #[serde(rename = "external_name")]. This tells Serde: "In the Rust world, this is user_id, but when you're talking to the wire, call it userId."
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct UserProfile {
// We want snake_case in Rust, but the API sends camelCase
#[serde(rename = "userId")]
user_id: u32,
#[serde(rename = "fullName")]
full_name: String,
}
fn main() {
let json = r#"{"userId": 101, "fullName": "Alice Smith"}"#;
let user: UserProfile = serde_json::from_str(json).unwrap();
println!("{:?}", user); // UserProfile { user_id: 101, full_name: "Alice Smith" }
}
If every single field in your struct needs to be converted from snake_case to camelCase, don't rename them one by one. You can put #[serde(rename_all = "camelCase")] at the top of the struct, and Serde will handle the translation for every field automatically. It's a massive time-saver.
Stop serializing internal state to the wire
Sometimes your struct needs fields that are essential for your program's logic but would be nonsensical (or a security risk) to send back to a client or save to a file. A common example is a cached calculation or a local session token.
If you leave these in, Serde will try to serialize them. Use #[serde(skip)] to tell Serde to completely ignore that field during both serialization and deserialization.
#[derive(Serialize, Deserialize, Debug)]
struct GameSession {
player_id: String,
score: u32,
// We need this for local logic, but we don't want to send it
// back to the server or save it in the save-game JSON.
#[serde(skip)]
local_cache_timestamp: u64,
}
One detail to watch out for: if you skip a field, Serde still needs to know what to put there when it deserializes JSON into that struct. It will use the type's Default implementation. If your type doesn't implement Default, you'll get a compile error. This leads us right into the next tool.
Stop relying solely on Option for missing keys
The instinct for many is to make every optional JSON field an Option<T>. While that's often correct, it forces you to unwrap or match on that option every single time you use the value in your code, even if there's a perfectly sensible default value.
#[serde(default)] allows you to say: "If this key is missing from the JSON, just use the default value for this type."
#[derive(Serialize, Deserialize, Debug)]
struct AppConfig {
host: String,
// If "port" is missing in the JSON, it defaults to 0 (u16's default)
#[serde(default)]
port: u16,
// You can also point to a custom function for a specific default
#[serde(default = "default_timeout")]
timeout_seconds: u32,
}
fn default_timeout() -> u32 { 30 }
fn main() {
let json = r#"{"host": "localhost"}"#;
let config: AppConfig = serde_json::from_str(json).unwrap();
println!("Port: {}, Timeout: {}", config.port, config.timeout_seconds);
// Port: 0, Timeout: 30
}
I personally prefer #[serde(default = "path")] over Option whenever a "sane default" exists. It cleans up the call sites of your functions because you're dealing with concrete values instead of wrapping everything in if let Some(...).
📋 Practical Task
Refactoring the Legacy Weather API Client
You are integrating a legacy Weather API. The API is inconsistent: it uses PascalCase for some fields, sends an internal debug_id that you must not send back to the server, and sometimes omits the units field (which should default to "metric").
Fix the WeatherResponse struct below. You must:
- Rename
TemperatureandHumidityto follow Rust'ssnake_caseconvention without triggering compiler warnings. - Ensure the
internal_process_idfield is completely ignored by Serde. - Ensure that if
unitsis missing from the JSON, it defaults to"metric"using a custom function.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct WeatherResponse {
// TODO: Rename this to 'temperature'
Temperature: f64,
// TODO: Rename this to 'humidity'
Humidity: f64,
// TODO: Ensure this is skipped during serialization/deserialization
internal_process_id: u64,
// TODO: Default this to "metric" if missing
units: String,
}
// TODO: Create the default function for units here
fn main() {
let json = r#"{"Temperature": 22.5, "Humidity": 60, "internal_process_id": 999}"#;
let weather: WeatherResponse = serde_json::from_str(json).expect("Failed to parse");
println!("{:?}", weather);
// Expected output: WeatherResponse { temperature: 22.5, humidity: 60.0, internal_process_id: 0, units: "metric" }
}
There are no comments for now.