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
164: Custom Serialization Logic
I just want one field to look different. Do I have to implement the whole Serialize trait manually?
Definitely not. Implementing Serialize by hand is a slog and, honestly, a bit of a minefield if you aren't familiar with the Serializer state machine. Most of the time, you just need a "bridge" function. Serde gives us the #[serde(serialize_with = "path")] attribute for exactly this reason.
Let's say you're working with a legacy API that expects coordinates not as an object, but as a single comma-separated string. You still want your Rust struct to have separate f64 fields for math, but the JSON needs to be "45.52,-122.67".
use serde::{Serialize, Serializer};
#[derive(Serialize)]
struct Point {
name: String,
#[serde(serialize_with = "serialize_coords")]
coords: Coordinate,
}
struct Coordinate {
lat: f64,
lon: f64,
}
fn serialize_coords(coord: &Coordinate, serializer: S) -> Result
where
S: Serializer,
{
let s = format!("{},{}", coord.lat, coord.lon);
serializer.serialize_str(&s)
}
I love this approach because it keeps the "weirdness" isolated in a small helper function. Your main data structures stay clean, and the serialization logic is decoupled from the struct definition.
When does it actually make sense to implement Serialize manually?
You should only go full manual when the structure of your serialized output is fundamentally different from the structure of your Rust type. If you're just changing a format (like the string example above), stick to attributes. But if you need to dynamically decide which fields to include based on the values of other fields, or if you're transforming a deeply nested tree into a flat list, that's when you implement the trait.
Just be warned: you'll be dealing with the Serializer trait directly. You aren't just returning a value; you're telling the serializer how to build the output. It's a bit like giving a set of assembly instructions to a robot.
impl Serialize for Coordinate {
fn serialize(&self, serializer: S) -> Result
where
S: Serializer,
{
// Instead of a map, we're just treating the whole struct as a string
serializer.serialize_str(&format!("{},{}", self.lat, self.lon))
}
}
In this case, I've made Coordinate itself serialize as a string. Now, any struct that contains a Coordinate will automatically use this logic without needing the serialize_with attribute on every single field. Use this if the type is "primitive" enough that it should always be represented this way.
Can I use the same helper function for both serialization and deserialization?
Unfortunately, no. The signatures are completely different. Serialization is about taking a reference to a value and pushing it into a Serializer. Deserialization is about taking a Deserializer and pulling a value out of it.
If you've used serialize_with, you'll almost certainly need a corresponding deserialize_with. The tricky part is that the deserializer doesn't know what it's looking at until it starts parsing. You usually have to deserialize into an intermediate type (like a String) and then parse that into your final type.
use serde::{Deserialize, Deserializer};
fn deserialize_coords<'de, D>(deserializer: D) -> Result
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let parts: Vec&str = s.split(',').collect();
if parts.len() != 2 {
return Err(serde::de::Error::custom("expected lat,lon"));
}
let lat = parts[0].parse().map_err(serde::de::Error::custom)?;
let lon = parts[1].parse().map_err(serde::de::Error::custom)?;
Ok(Coordinate { lat, lon })
}
It's a bit more boilerplate, but it's the only way to maintain type safety. You're essentially writing a mini-parser for that specific field.
📋 Practical Task
Exercise: Implementing a Unix Timestamp Formatter
You are integrating with an API that provides timestamps as seconds since the epoch (integers), but your Rust application uses a DateTime-like wrapper for better type safety. Your goal is to create a custom serialization pair that converts a Timestamp struct into a plain u64 during serialization, and back again during deserialization.
- Create a struct
Timestamp(u64). - Create a struct
Eventthat contains aname: Stringand atime: Timestamp. - Implement a
serialize_timestamphelper that serializes theTimestampwrapper as a rawu64. - Implement a
deserialize_timestamphelper that takes au64from the JSON and wraps it back into theTimestampstruct. - Ensure that when you serialize
Event { name: "Login".into(), time: Timestamp(1672531200) }, the resulting JSON is{"name": "Login", "time": 1672531200}and NOT{"name": "Login", "time": {"0": 1672531200}}.
There are no comments for now.