Full title: [Advanced Rust] 2.3. API Design Principles of Unsurprising Pt.3 - Implementing serde Serialize and Deserialize Traits, and Why Copy Is Not Recommended
2.3.1. It Is Recommended to Implement Serialize and Deserialize in serde
Serde is the core Rust library for serialization and deserialization:
- Serialization: converts a Rust struct or enum into a string or binary representation such as JSON or YAML
- Deserialization: parses a string or binary representation such as JSON or YAML back into a Rust struct or enum
Serialize and Deserialize are both traits from the serde crate.
Serialize Trait
The Serialize trait allows a type to be converted into a serializable data format such as JSON, YAML, or TOML.
Its main methods include:
serialize_boolserialize_i32serialize_strserialize_struct
These are methods on the Serializer (and related) traits that a Serialize implementation calls; the Serialize trait itself only requires serialize.
Its definition is:
pub trait Serialize {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer;
}
Enter fullscreen mode Exit fullscreen mode
Here is an example showing how to implement Serialize manually:
use serde::ser::{Serialize, SerializeStruct, Serializer};
struct Point {
x: i32,
y: i32,
}
impl Serialize for Point {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("Point", 2)?;
state.serialize_field("x", &self.x)?;
state.serialize_field("y", &self.y)?;
state.end()
}
}
Enter fullscreen mode Exit fullscreen mode
-
serializer.serialize_struct("Point", 2)?creates a struct serializer state, and2is the number of fields -
state.serialize_field("x", &self.x)?serializes the struct fields one by one -
state.end()finishes serialization
Deserialize Trait
The Deserialize trait allows Rust types to be parsed from various data formats.
Its main methods include:
deserialize_booldeserialize_i32deserialize_stringdeserialize_struct
These are methods on the Deserializer (and related) traits that a Deserialize implementation drives via a Visitor; the Deserialize trait itself only requires deserialize.
Its definition is:
pub trait Deserialize<'de>: Sized {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>;
}
Enter fullscreen mode Exit fullscreen mode
- The purpose of
deserializeis to usedeserializerto parse a Rust type from some data format
Here is an example showing how to implement Deserialize manually:
use serde::de::{self, Deserialize, Deserializer, Visitor, MapAccess};
use std::fmt;
struct Point {
x: i32,
y: i32,
}
impl<'de> Deserialize<'de> for Point {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct PointVisitor;
impl<'de> Visitor<'de> for PointVisitor {
type Value = Point;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a struct Point with fields x and y")
}
fn visit_map<M>(self, mut map: M) -> Result<Point, M::Error>
where
M: MapAccess<'de>,
{
let mut x = None;
let mut y = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"x" => x = Some(map.next_value()?),
"y" => y = Some(map.next_value()?),
_ => {}
}
}
let x = x.ok_or_else(|| de::Error::missing_field("x"))?;
let y = y.ok_or_else(|| de::Error::missing_field("y"))?;
Ok(Point { x, y })
}
}
deserializer.deserialize_struct("Point", &["x", "y"], PointVisitor)
}
}
Enter fullscreen mode Exit fullscreen mode
-
PointVisitoris used to parse JSON fields -
visit_mapparses the values ofxandyand ensures the fields exist
Automatically Implementing Serialize and Deserialize with #[derive(Serialize, Deserialize)]
Manually implementing Serialize and Deserialize is very tedious, so we usually use the serde_derive macro:
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 10, y: 20 };
let serialized = serde_json::to_string(&p).unwrap();
println!("Serialized: {}", serialized); // {"x":10,"y":20}
let deserialized: Point = serde_json::from_str(&serialized).unwrap();
println!("Deserialized: {:?}", deserialized); // Point { x: 10, y: 20 }
}
Enter fullscreen mode Exit fullscreen mode
Example Using Serialize and Deserialize
use serde::{Serialize, Deserialize};
use serde_json;
#[derive(Serialize, Deserialize, Debug)]
struct User {
name: String,
age: u32,
}
fn main() {
let user = User {
name: "Alice".to_string(),
age: 30,
};
// Serialize
let json_str = serde_json::to_string(&user).unwrap();
println!("Serialized JSON: {}", json_str);
// Deserialize
let deserialized: User = serde_json::from_str(&json_str).unwrap();
println!("Deserialized: {:?}", deserialized);
}
Enter fullscreen mode Exit fullscreen mode
Output:
Serialized JSON: {"name":"Alice","age":30}
Deserialized: User { name: "Alice", age: 30 }
Enter fullscreen mode Exit fullscreen mode
Other Notes
serde's serde_derive crate provides a mechanism for overriding the serialization of individual fields or enum variants. Because serde is a third-party library, you may not want to force it as a dependency.
Most libraries choose to provide a serde feature, and only add serde support when the user enables that feature.
That means you can write this in your crate:
[dependencies]
serde = { version = "1.0", optional = true }
[features]
serde = ["dep:serde"]
Enter fullscreen mode Exit fullscreen mode
- In the
[dependencies]section,serdeis introduced as a semver dependency withoptional = true, which means it is an optional dependency. This means serde is not included by default unless it is explicitly enabled - In the
[features]section,serde = ["dep:serde"]means that when the user enables theserdefeature, theserdedependency will also be enabled -
"dep:serde"indicates that this feature depends on the serde dependency (dep:is the dependency prefix that tells Cargo this feature depends on serde)
Someone else can enable serde when using your crate (assuming the crate is named my_crate) like this:
[dependencies]
my_crate = { version = "0.1", features = ["serde"] }
Enter fullscreen mode Exit fullscreen mode
2.3.2. It Is Not Recommended to Implement the Copy Trait
Users usually do not expect a type to implement Copy. If they want a duplicate, they usually call clone.
The Copy trait changes the semantics of moving a value of a given type. Example:
#[derive(Debug, Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let point1 = Point { x: 10, y: 10 };
let point2 = point1;
println!("{:?}", point1);
println!("{:?}", point2);
}
Enter fullscreen mode Exit fullscreen mode
The value of point1 is assigned to point2. Normally, point1 would be invalid after that, but because the Point struct implements Copy, the assignment copies the value instead of moving it, and point1 remains valid. That can surprise users, so implementing Copy is not recommended.
In addition, types that implement Copy have many restrictions. A type that starts out simple can easily stop meeting the requirements for Copy. For example, once it contains a String or any other type that does not support Copy, you must remove Copy.
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.