How do you implement custom de/serialization with serde using Serializer and Deserializer traits?

Serde's Serializer and Deserializer traits define the interface between Rust data structures and data formats like JSON, YAML, or binary formats. Implementing these traits gives you complete control over how types are serialized and deserialized, enabling custom encoding schemes, format-specific optimizations, or handling types that don't derive Serialize and Deserialize automatically. The traits are large but follow consistent patterns.

The Serialize Trait

use serde::{Serialize, Serializer, ser::SerializeStruct};
 
// Custom serialization for a type
struct User {
    id: u64,
    username: String,
    email: String,
    is_admin: bool,
}
 
impl Serialize for User {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // Serialize as a struct with named fields
        let mut s = serializer.serialize_struct("User", 4)?;
        s.serialize_field("id", &self.id)?;
        s.serialize_field("username", &self.username)?;
        s.serialize_field("email", &self.email)?;
        s.serialize_field("is_admin", &self.is_admin)?;
        s.end()
    }
}

The Serialize trait controls how a type produces output for any format.

The Deserialize Trait

use serde::{Deserialize, Deserializer, de::Error as DeError};
use std::fmt;
 
struct User {
    id: u64,
    username: String,
    email: String,
    is_admin: bool,
}
 
impl<'de> Deserialize<'de> for User {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        // Define a visitor to handle the deserialization
        struct UserVisitor;
 
        impl<'de> serde::de::Visitor<'de> for UserVisitor {
            type Value = User;
 
            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                write!(formatter, "struct User")
            }
 
            fn visit_map<V>(self, mut map: V) -> Result<User, V::Error>
            where
                V: serde::de::MapAccess<'de>,
            {
                let mut id = None;
                let mut username = None;
                let mut email = None;
                let mut is_admin = None;
 
                while let Some(key) = map.next_key::<String>()? {
                    match key.as_str() {
                        "id" => id = Some(map.next_value()?),
                        "username" => username = Some(map.next_value()?),
                        "email" => email = Some(map.next_value()?),
                        "is_admin" => is_admin = Some(map.next_value()?),
                        _ => return Err(DeError::unknown_field(&key, &["id", "username", "email", "is_admin"])),
                    }
                }
 
                let id = id.ok_or_else(|| DeError::missing_field("id"))?;
                let username = username.ok_or_else(|| DeError::missing_field("username"))?;
                let email = email.ok_or_else(|| DeError::missing_field("email"))?;
                let is_admin = is_admin.ok_or_else(|| DeError::missing_field("is_admin"))?;
 
                Ok(User { id, username, email, is_admin })
            }
        }
 
        // Use the visitor to deserialize
        deserializer.deserialize_struct("User", &["id", "username", "email", "is_admin"], UserVisitor)
    }
}

Deserialization requires a visitor pattern to handle the incoming data.

Serializing Simple Types

use serde::{Serializer, Serialize};
 
// Serialize a simple wrapper
struct UserId(u64);
 
impl Serialize for UserId {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // Serialize as a string instead of a number
        serializer.serialize_str(&self.0.to_string())
    }
}
 
// Serialize an enum
enum Status {
    Active,
    Inactive,
    Pending,
}
 
impl Serialize for Status {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        match self {
            Status::Active => serializer.serialize_str("active"),
            Status::Inactive => serializer.serialize_str("inactive"),
            Status::Pending => serializer.serialize_str("pending"),
        }
    }
}

Simple types can serialize directly to primitive values.

Deserializing Simple Types

use serde::{Deserializer, de::Visitor};
use std::fmt;
 
struct UserId(u64);
 
impl<'de> Deserialize<'de> for UserId {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct UserIdVisitor;
 
        impl<'de> Visitor<'de> for UserIdVisitor {
            type Value = UserId;
 
            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                write!(formatter, "a string containing a u64")
            }
 
            fn visit_str<E>(self, v: &str) -> Result<UserId, E>
            where
                E: serde::de::Error,
            {
                v.parse::<u64>()
                    .map(UserId)
                    .map_err(|_| E::invalid_value(serde::de::Unexpected::Str(v), &self))
            }
        }
 
        deserializer.deserialize_string(UserIdVisitor)
    }
}

The visitor handles string input and parses it to the inner type.

Custom Field Serialization

use serde::{Serialize, Serializer, ser::SerializeStruct};
 
// Serialize with computed/derived fields
struct Product {
    name: String,
    price_cents: u64,  // Store as cents, serialize as dollars
}
 
impl Serialize for Product {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut s = serializer.serialize_struct("Product", 3)?;
        s.serialize_field("name", &self.name)?;
        s.serialize_field("price", &(self.price_cents as f64 / 100.0))?;
        s.serialize_field("price_formatted", &format!("${:.2}", self.price_cents as f64 / 100.0))?;
        s.end()
    }
}
 
fn serialize_example() {
    let product = Product {
        name: "Widget".to_string(),
        price_cents: 1999,
    };
    
    let json = serde_json::to_string(&product).unwrap();
    println!("{}", json);
    // {"name":"Widget","price":19.99,"price_formatted":"$19.99"}
}

Serialize can transform data during output.

Custom Field Deserialization

use serde::{Deserialize, Deserializer, de::{Visitor, Error, MapAccess}};
use std::fmt;
 
#[derive(Debug)]
struct Product {
    name: String,
    price_cents: u64,
}
 
impl<'de> Deserialize<'de> for Product {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct ProductVisitor;
 
        impl<'de> Visitor<'de> for ProductVisitor {
            type Value = Product;
 
            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
                write!(f, "a Product object")
            }
 
            fn visit_map<V>(self, mut map: V) -> Result<Product, V::Error>
            where
                V: MapAccess<'de>,
            {
                let mut name = None;
                let mut price_cents = None;
 
                while let Some(key) = map.next_key::<String>()? {
                    match key.as_str() {
                        "name" => name = Some(map.next_value()?),
                        "price" => {
                            // Accept price as float, convert to cents
                            let price: f64 = map.next_value()?;
                            price_cents = Some((price * 100.0) as u64);
                        }
                        "price_cents" => price_cents = Some(map.next_value()?),
                        _ => return Err(Error::unknown_field(&key, &["name", "price", "price_cents"])),
                    }
                }
 
                let name = name.ok_or_else(|| Error::missing_field("name"))?;
                let price_cents = price_cents.ok_or_else(|| Error::missing_field("price or price_cents"))?;
 
                Ok(Product { name, price_cents })
            }
        }
 
        deserializer.deserialize_struct("Product", &["name", "price", "price_cents"], ProductVisitor)
    }
}

Deserialize can accept multiple input formats and normalize them.

Serializing Enums with Custom Representation

use serde::{Serializer, ser::SerializeTupleVariant};
 
enum Event {
    Click { x: i32, y: i32 },
    KeyPress(char),
    Resize { width: u32, height: u32 },
}
 
impl Serialize for Event {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        match self {
            Event::Click { x, y } => {
                // Serialize as ["click", x, y]
                let mut tuple = serializer.serialize_tuple_variant("Event", 0, "click", 2)?;
                tuple.serialize_field(x)?;
                tuple.serialize_field(y)?;
                tuple.end()
            }
            Event::KeyPress(c) => {
                // Serialize as ["keyPress", c]
                let mut tuple = serializer.serialize_tuple_variant("Event", 1, "keyPress", 1)?;
                tuple.serialize_field(c)?;
                tuple.end()
            }
            Event::Resize { width, height } => {
                // Serialize as {"resize": {"width": w, "height": h}}
                let mut s = serializer.serialize_newtype_variant("Event", 2, "resize", "Resize")?;
                let mut inner = s.serialize_struct("Resize", 2)?;
                inner.serialize_field("width", width)?;
                inner.serialize_field("height", height)?;
                inner.end()
            }
        }
    }
}

Enums can serialize to various representations depending on format requirements.

Deserializing Enums

use serde::{Deserializer, de::{Visitor, Error, VariantAccess, SeqAccess}};
use std::fmt;
 
impl<'de> Deserialize<'de> for Event {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct EventVisitor;
 
        impl<'de> Visitor<'de> for EventVisitor {
            type Value = Event;
 
            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
                write!(f, "an Event")
            }
 
            fn visit_seq<V>(self, mut seq: V) -> Result<Event, V::Error>
            where
                V: SeqAccess<'de>,
            {
                // Expect ["eventName", ...fields]
                let event_type: String = seq.next_element()?
                    .ok_or_else(|| Error::missing_field("event type"))?;
 
                match event_type.as_str() {
                    "click" => {
                        let x = seq.next_element()?
                            .ok_or_else(|| Error::missing_field("x"))?;
                        let y = seq.next_element()?
                            .ok_or_else(|| Error::missing_field("y"))?;
                        Ok(Event::Click { x, y })
                    }
                    "keyPress" => {
                        let c = seq.next_element()?
                            .ok_or_else(|| Error::missing_field("char"))?;
                        Ok(Event::KeyPress(c))
                    }
                    "resize" => {
                        let width = seq.next_element()?
                            .ok_or_else(|| Error::missing_field("width"))?;
                        let height = seq.next_element()?
                            .ok_or_else(|| Error::missing_field("height"))?;
                        Ok(Event::Resize { width, height })
                    }
                    _ => Err(Error::unknown_variant(&event_type, &["click", "keyPress", "resize"]))
                }
            }
        }
 
        deserializer.deserialize_seq(EventVisitor)
    }
}
 
enum Event {
    Click { x: i32, y: i32 },
    KeyPress(char),
    Resize { width: u32, height: u32 },
}

Enum deserialization typically handles multiple input formats.

Implementing a Custom Serializer

use serde::ser::{Serializer, SerializeStruct, SerializeTuple, Error};
 
// A simple serializer that outputs to a string
struct StringSerializer {
    output: String,
}
 
impl StringSerializer {
    fn new() -> Self {
        StringSerializer { output: String::new() }
    }
}
 
impl Serializer for StringSerializer {
    type Ok = String;
    type Error = serde::ser::Error;
    type SerializeStruct = StructSerializer;
    type SerializeTuple = TupleSerializer;
    // ... other associated types
 
    fn serialize_bool(self, v: bool) -> Result<Self::Ok, Self::Error> {
        Ok(v.to_string())
    }
 
    fn serialize_i64(self, v: i64) -> Result<Self::Ok, Self::Error> {
        Ok(v.to_string())
    }
 
    fn serialize_u64(self, v: u64) -> Result<Self::Ok, Self::Error> {
        Ok(v.to_string())
    }
 
    fn serialize_str(self, v: &str) -> Result<Self::Ok, Self::Error> {
        Ok(format!("\"{}\"", v))
    }
 
    fn serialize_struct(
        self,
        _name: &'static str,
        _len: usize,
    ) -> Result<Self::SerializeStruct, Self::Error> {
        Ok(StructSerializer { output: String::new() })
    }
 
    fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Self::Error> {
        Ok(TupleSerializer { elements: Vec::new() })
    }
 
    // Simplified for example - real serializer needs all methods
    type SerializeSeq = serde::ser::Impossible<String, Self::Error>;
    type SerializeTupleStruct = serde::ser::Impossible<String, Self::Error>;
    type SerializeTupleVariant = serde::ser::Impossible<String, Self::Error>;
    type SerializeMap = serde::ser::Impossible<String, Self::Error>;
    type SerializeStructVariant = serde::ser::Impossible<String, Self::Error>;
 
    fn serialize_i8(self, v: i8) -> Result<Self::Ok, Self::Error> { self.serialize_i64(v as i64) }
    fn serialize_i16(self, v: i16) -> Result<Self::Ok, Self::Error> { self.serialize_i64(v as i64) }
    fn serialize_i32(self, v: i32) -> Result<Self::Ok, Self::Error> { self.serialize_i64(v as i64) }
    fn serialize_u8(self, v: u8) -> Result<Self::Ok, Self::Error> { self.serialize_u64(v as u64) }
    fn serialize_u16(self, v: u16) -> Result<Self::Ok, Self::Error> { self.serialize_u64(v as u64) }
    fn serialize_u32(self, v: u32) -> Result<Self::Ok, Self::Error> { self.serialize_u64(v as u64) }
    fn serialize_f32(self, v: f32) -> Result<Self::Ok, Self::Error> { Ok(v.to_string()) }
    fn serialize_f64(self, v: f64) -> Result<Self::Ok, Self::Error> { Ok(v.to_string()) }
    fn serialize_char(self, v: char) -> Result<Self::Ok, Self::Error> { Ok(format!("'{}'", v)) }
    fn serialize_bytes(self, v: &[u8]) -> Result<Self::Ok, Self::Error> { 
        Ok(format!("{:?}", v))
    }
    fn serialize_none(self) -> Result<Self::Ok, Self::Error> { Ok("null".to_string()) }
    fn serialize_some<T>(self, value: &T) -> Result<Self::Ok, Self::Error>
    where
        T: ?Sized + Serialize,
    {
        value.serialize(self)
    }
    fn serialize_unit(self) -> Result<Self::Ok, Self::Error> { Ok("()".to_string()) }
    fn serialize_unit_struct(self, name: &'static str) -> Result<Self::Ok, Self::Error> {
        Ok(name.to_string())
    }
    fn serialize_unit_variant(
        self,
        _name: &'static str,
        _variant_index: u32,
        variant_name: &'static str,
    ) -> Result<Self::Ok, Self::Error> {
        Ok(variant_name.to_string())
    }
    fn serialize_newtype_struct<T>(
        self,
        _name: &'static str,
        value: &T,
    ) -> Result<Self::Ok, Self::Error>
    where
        T: ?Sized + Serialize,
    {
        value.serialize(self)
    }
    fn serialize_newtype_variant<T>(
        self,
        _name: &'static str,
        _variant_index: u32,
        variant_name: &'static str,
        value: &T,
    ) -> Result<Self::Ok, Self::Error>
    where
        T: ?Sized + Serialize,
    {
        Ok(format!("{}({})", variant_name, value.serialize(Self::new())?))
    }
    fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
        Err(Self::Error::custom("seq not supported"))
    }
    fn serialize_tuple_struct(self, _name: &'static str, _len: usize) -> Result<Self::SerializeTupleStruct, Self::Error> {
        Err(Self::Error::custom("tuple struct not supported"))
    }
    fn serialize_tuple_variant(
        self,
        _name: &'static str,
        _variant_index: u32,
        _variant_name: &'static str,
        _len: usize,
    ) -> Result<Self::SerializeTupleVariant, Self::Error> {
        Err(Self::Error::custom("tuple variant not supported"))
    }
    fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
        Err(Self::Error::custom("map not supported"))
    }
    fn serialize_struct_variant(
        self,
        _name: &'static str,
        _variant_index: u32,
        _variant_name: &'static str,
        _len: usize,
    ) -> Result<Self::SerializeStructVariant, Self::Error> {
        Err(Self::Error::custom("struct variant not supported"))
    }
}
 
struct StructSerializer {
    output: String,
}
 
impl SerializeStruct for StructSerializer {
    type Ok = String;
    type Error = serde::ser::Error;
 
    fn serialize_field<T: ?Sized + Serialize>(
        &mut self,
        key: &'static str,
        value: &T,
    ) -> Result<(), Self::Error> {
        if !self.output.is_empty() {
            self.output.push_str(", ");
        }
        self.output.push_str(key);
        self.output.push_str(": ");
        self.output.push_str(&value.serialize(StringSerializer::new())?);
        Ok(())
    }
 
    fn end(self) -> Result<Self::Ok, Self::Error> {
        Ok(format!("{{{}}}", self.output))
    }
}
 
struct TupleSerializer {
    elements: Vec<String>,
}
 
impl SerializeTuple for TupleSerializer {
    type Ok = String;
    type Error = serde::ser::Error;
 
    fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
        self.elements.push(value.serialize(StringSerializer::new())?);
        Ok(())
    }
 
    fn end(self) -> Result<Self::Ok, Self::Error> {
        Ok(format!("({})", self.elements.join(", ")))
    }
}

Implementing a full serializer requires handling all possible data shapes.

Visitor Pattern for Deserialization

use serde::de::{Visitor, Error};
 
// A visitor that can handle multiple input types
struct FlexibleIntVisitor;
 
impl<'de> Visitor<'de> for FlexibleIntVisitor {
    type Value = u64;
 
    fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "an integer or a string containing an integer")
    }
 
    // Handle integer directly
    fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
    where
        E: Error,
    {
        Ok(v)
    }
 
    fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
    where
        E: Error,
    {
        if v >= 0 {
            Ok(v as u64)
        } else {
            Err(E::custom("expected positive integer"))
        }
    }
 
    // Handle string containing integer
    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
    where
        E: Error,
    {
        v.parse::<u64>()
            .map_err(|_| E::invalid_value(serde::de::Unexpected::Str(v), &self))
    }
}
 
// Using the visitor
fn deserialize_flexible_int<'de, D>(deserializer: D) -> Result<u64, D::Error>
where
    D: Deserializer<'de>,
{
    deserializer.deserialize_any(FlexibleIntVisitor)
}

Visitors can accept multiple input formats for the same data.

Using with Serde Attributes

use serde::{Serialize, Deserialize};
 
// Combine custom serialization with serde attributes
#[derive(Serialize, Deserialize)]
struct Config {
    name: String,
    
    #[serde(serialize_with = "serialize_version")]
    #[serde(deserialize_with = "deserialize_version")]
    version: Version,
    
    #[serde(skip_serializing_if = "Option::is_none")]
    description: Option<String>,
}
 
struct Version {
    major: u32,
    minor: u32,
    patch: u32,
}
 
fn serialize_version<S>(v: &Version, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    serializer.serialize_str(&format!("{}.{}.{}", v.major, v.minor, v.patch))
}
 
fn deserialize_version<'de, D>(deserializer: D) -> Result<Version, D::Error>
where
    D: Deserializer<'de>,
{
    let s = String::deserialize(deserializer)?;
    let parts: Vec<&str> = s.split('.').collect();
    if parts.len() != 3 {
        return Err(serde::de::Error::custom("invalid version format"));
    }
    Ok(Version {
        major: parts[0].parse().map_err(serde::de::Error::custom)?,
        minor: parts[1].parse().map_err(serde::de::Error::custom)?,
        patch: parts[2].parse().map_err(serde::de::Error::custom)?,
    })
}

Custom serialization can be combined with derive macros via attributes.

Handling External Types

use serde::{Serializer, Deserialize, Deserializer, de::Visitor};
use std::fmt;
use std::net::IpAddr;
 
// Serialize IpAddr in a custom format
fn serialize_ip<S>(ip: &IpAddr, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    // Could serialize with extra info or in a specific format
    serializer.serialize_str(&ip.to_string())
}
 
fn deserialize_ip<'de, D>(deserializer: D) -> Result<IpAddr, D::Error>
where
    D: Deserializer<'de>,
{
    struct IpAddrVisitor;
 
    impl<'de> Visitor<'de> for IpAddrVisitor {
        type Value = IpAddr;
 
        fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
            write!(f, "an IP address string")
        }
 
        fn visit_str<E>(self, v: &str) -> Result<IpAddr, E>
        where
            E: serde::de::Error,
        {
            v.parse::<IpAddr>()
                .map_err(|_| E::invalid_value(serde::de::Unexpected::Str(v), &self))
        }
    }
 
    deserializer.deserialize_str(IpAddrVisitor)
}
 
// Usage in a struct
#[derive(serde::Serialize, serde::Deserialize)]
struct Server {
    #[serde(serialize_with = "serialize_ip")]
    #[serde(deserialize_with = "deserialize_ip")]
    bind_address: IpAddr,
    port: u16,
}

Custom serialization works around orphan rules for external types.

Synthesis

Custom serialization with Serializer and Deserializer traits provides complete control:

Key patterns:

Task Approach
Serialize a type Implement Serialize trait
Deserialize a type Implement Deserialize with a Visitor
Serialize one field Use serialize_with attribute
Deserialize one field Use deserialize_with attribute
Accept multiple formats Visitor with multiple visit_* methods
Custom format Implement full Serializer/Deserializer

Visitor methods:

Method Handles
visit_bool true/false
visit_i64, visit_u64 Numbers
visit_str String values
visit_seq Array/sequence
visit_map Object/map
visit_none, visit_some Optional values

When to implement custom serialization:

  • External types that don't implement Serialize/Deserialize
  • Custom encoding schemes or format requirements
  • Accepting multiple input formats
  • Computed or derived fields
  • Backward-compatible format changes

Key insight: The Serializer trait is about producing output while Deserializer is about consuming input. Serialization is straightforward—write methods that call the serializer with your data. Deserialization requires the visitor pattern because the deserializer drives the process, asking the visitor what to do with each piece of data it encounters.