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_bool
  • serialize_i32
  • serialize_str
  • serialize_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, and 2 is 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_bool
  • deserialize_i32
  • deserialize_string
  • deserialize_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 deserialize is to use deserializer to 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

  • PointVisitor is used to parse JSON fields
  • visit_map parses the values of x and y and 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, serde is introduced as a semver dependency with optional = 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 the serde feature, the serde dependency 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.