What is the purpose of serde::de::Visitor::visit_bytes for deserializing borrowed byte data?

visit_bytes is a method on the Visitor trait that deserializers call when they encounter byte data that can be borrowed from the input source, enabling zero-copy deserialization where the deserialized value references the original input bytes without allocation. This is distinct from visit_byte_buf, which is called when bytes must be owned (allocated), and the choice between them determines whether deserialization can avoid copying byte data.

The Visitor Trait and Byte Handling

use serde::de::{Visitor, Error};
use std::fmt;
 
// The Visitor trait has multiple visit_* methods for different data types
// visit_bytes handles borrowed byte slices: &[u8]
// visit_byte_buf handles owned byte vectors: Vec<u8>
 
struct BytesVisitor;
 
impl<'de> Visitor<'de> for BytesVisitor {
    type Value = &'de [u8];
 
    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        write!(formatter, "a byte slice")
    }
 
    // Called when deserializer has borrowable bytes
    fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
    where
        E: Error,
    {
        // v is borrowed from the input
        // We can return it as &'de [u8]
        Ok(v)
    }
 
    // Called when deserializer must allocate
    fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
    where
        E: Error,
    {
        // v is owned Vec<u8>
        // Cannot convert to &'de [u8] - lifetime mismatch!
        // This would be an error for a borrowed result
        Err(Error::custom("expected borrowed bytes"))
    }
}

visit_bytes receives &[u8] borrowed from input; visit_byte_buf receives Vec<u8> owned by the visitor.

Zero-Copy Deserialization

use serde::Deserialize;
use std::borrow::Cow;
 
// Zero-copy: borrowing from input without allocation
#[derive(Deserialize)]
struct BorrowedData<'a> {
    // This field borrows from the input
    #[serde(borrow)]
    data: &'a [u8],
}
 
// For zero-copy to work, the input must outlive the result
// This works with formats that support borrowing (not JSON strings)
fn zero_copy_example() {
    // Binary formats like bincode can support borrowed bytes
    // The bytes in the input are referenced directly
    
    // JSON strings cannot borrow - they require decoding
    // JSON would call visit_byte_buf, not visit_bytes
}
 
// Cow allows either borrowed or owned
#[derive(Deserialize)]
struct FlexibleData<'a> {
    #[serde(borrow)]
    data: Cow<'a, [u8]>,
}

Zero-copy deserialization requires format support; not all formats can provide borrowed bytes.

Borrowed vs Owned Byte Deserialization

use serde::de::{Deserialize, Deserializer, Visitor, Error};
use std::fmt;
 
struct BytesOrBufVisitor;
 
impl<'de> Visitor<'de> for BytesOrBufVisitor {
    type Value = Cow<'de, [u8]>;
 
    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        write!(formatter, "bytes")
    }
 
    // Borrowed bytes - zero-copy
    fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
    where
        E: Error,
    {
        // Borrow from input, no allocation
        Ok(Cow::Borrowed(v))
    }
 
    // Owned bytes - requires allocation
    fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
    where
        E: Error,
    {
        // Must allocate, input cannot be borrowed
        Ok(Cow::Owned(v))
    }
 
    // Also need to handle string forms for JSON compatibility
    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
    where
        E: Error,
    {
        // JSON often encodes bytes as base64 strings
        // Must allocate to decode
        Ok(Cow::Owned(v.as_bytes().to_vec()))
    }
}

Cow<'de, [u8]> enables handling both borrowed and owned byte data.

When Deserializers Call visit_bytes

use serde::de::{Deserializer, Visitor};
 
// Deserializers decide when to call visit_bytes vs visit_byte_buf:
//
// 1. Binary formats (bincode, postcard):
//    - Can provide borrowed bytes when the input is a byte slice
//    - Call visit_bytes for byte slices in the input
//    - Call visit_byte_buf when copying is needed
//
// 2. JSON:
//    - Strings require base64 decoding for byte data
//    - Always calls visit_str or visit_string, not visit_bytes
//    - Byte deserialization from JSON typically uses base64
//
// 3. Self-describing formats:
//    - May provide borrowed bytes when possible
//    - Fall back to visit_byte_buf when necessary
 
fn deserialize_bytes_example<'de, D>(deserializer: D) -> Result<&'de [u8], D::Error>
where
    D: Deserializer<'de>,
{
    // This tells the deserializer we want borrowed bytes
    // The deserializer will call visit_bytes if it can
    // Otherwise it may call visit_byte_buf (which would fail here)
    deserializer.deserialize_bytes(BytesSliceVisitor)
}
 
struct BytesSliceVisitor;
 
impl<'de> Visitor<'de> for BytesSliceVisitor {
    type Value = &'de [u8];
 
    fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "a borrowed byte slice")
    }
 
    fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
    where
        E: serde::de::Error,
    {
        Ok(v)  // Return borrowed reference
    }
 
    fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
    where
        E: serde::de::Error,
    {
        // Cannot convert owned Vec to borrowed slice
        Err(Error::custom("expected borrowed bytes, got owned bytes"))
    }
}

Deserializers call visit_bytes when they can lend bytes from the input.

Format Support for Borrowed Bytes

use serde::Deserialize;
 
// Different formats have different capabilities:
//
// bincode:
// - Supports borrowed bytes when deserializing from &[u8]
// - Input must outlive the deserialized value
// - Calls visit_bytes for byte fields
//
// postcard:
// - Similar to bincode, supports borrowing from &[u8]
//
// JSON (serde_json):
// - Strings are UTF-8 and may need escape processing
// - Cannot generally provide borrowed bytes for arbitrary byte data
// - Byte data typically encoded as base64 strings
// - Calls visit_str/visit_string, not visit_bytes
//
// MessagePack (rmp-serde):
// - Can support borrowed bytes for bin type
// - Depends on whether input is borrowed
 
// Example: bincode supports borrowing
fn bincode_borrowing() {
    let data: Vec<u8> = vec![1, 2, 3, 4, 5];
    let encoded = bincode::serialize(&data).unwrap();
    
    // Deserialize with borrowed bytes
    let borrowed: &[u8] = bincode::deserialize(&encoded).unwrap();
    // borrowed points into encoded vector
}
 
// JSON cannot borrow bytes directly
fn json_no_borrow() {
    let data: Vec<u8> = vec![1, 2, 3, 4, 5];
    let encoded = serde_json::to_string(&data).unwrap();
    
    // This would NOT work:
    // let borrowed: &[u8] = serde_json::from_str(&encoded).unwrap();
    // Error: JSON cannot provide borrowed bytes
    
    // Must deserialize to owned:
    let owned: Vec<u8> = serde_json::from_str(&encoded).unwrap();
}

Format support for borrowed bytes depends on input ownership and encoding.

Implementing visit_bytes in Custom Visitors

use serde::de::{Visitor, Error};
use std::fmt;
 
// A visitor that expects exactly 4 borrowed bytes
struct FourBytesVisitor;
 
impl<'de> Visitor<'de> for FourBytesVisitor {
    type Value = &'de [u8; 4];
 
    fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "exactly 4 bytes")
    }
 
    fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
    where
        E: Error,
    {
        // Try to convert slice to array reference
        if v.len() == 4 {
            // Safe: we just checked the length
            let array: &'de [u8; 4] = v.try_into().map_err(|_| {
                Error::custom("internal error: length check failed")
            }).ok().unwrap();
            
            // Actually, try_into returns an owned array
            // For borrowed, use:
            let array: &'de [u8; 4] = v.try_into().map_err(|_| {
                Error::invalid_length(v.len(), &"4 bytes")
            })?;
            Ok(array)
        } else {
            Err(Error::invalid_length(v.len(), &"4 bytes"))
        }
    }
 
    fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
    where
        E: Error,
    {
        // Cannot return borrowed reference to owned data
        Err(Error::custom("expected borrowed bytes, not owned"))
    }
}

Custom visitors can validate borrowed bytes during deserialization.

Handling Both Borrowed and Owned

use serde::de::{Visitor, Error};
use std::fmt;
use std::borrow::Cow;
 
// A flexible visitor that handles both cases
struct FlexibleBytesVisitor;
 
impl<'de> Visitor<'de> for FlexibleBytesVisitor {
    type Value = Cow<'de, [u8]>;
 
    fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "bytes")
    }
 
    fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
    where
        E: Error,
    {
        // Zero-copy: borrow from input
        Ok(Cow::Borrowed(v))
    }
 
    fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
    where
        E: Error,
    {
        // Owned: must copy
        Ok(Cow::Owned(v))
    }
 
    // Also handle string inputs (common for JSON)
    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
    where
        E: Error,
    {
        // String as bytes - must copy for owned result
        // Note: This might not be valid UTF-8 handling
        Ok(Cow::Owned(v.as_bytes().to_vec()))
    }
 
    fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
    where
        E: Error,
    {
        // Owned string - convert to owned bytes
        Ok(Cow::Owned(v.into_bytes()))
    }
}

A well-designed visitor handles both borrowed and owned inputs gracefully.

Deserialize Trait Implementation

use serde::{Deserialize, Deserializer};
use std::borrow::Cow;
 
// Custom type that stores borrowed bytes when possible
struct MaybeBorrowedBytes<'a> {
    data: Cow<'a, [u8]>,
}
 
impl<'de> Deserialize<'de> for MaybeBorrowedBytes<'de> {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        // Use our flexible visitor
        struct BytesVisitor;
 
        impl<'de> Visitor<'de> for BytesVisitor {
            type Value = MaybeBorrowedBytes<'de>;
 
            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
                write!(f, "bytes")
            }
 
            fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                Ok(MaybeBorrowedBytes {
                    data: Cow::Borrowed(v),
                })
            }
 
            fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                Ok(MaybeBorrowedBytes {
                    data: Cow::Owned(v),
                })
            }
        }
 
        deserializer.deserialize_bytes(BytesVisitor)
    }
}

Implement Deserialize using deserialize_bytes to request borrowed data.

The deserialize_bytes Method

use serde::Deserializer;
 
// Deserializer::deserialize_bytes is the key method:
// It hints to the deserializer that the visitor wants bytes
// The deserializer then decides whether to call:
// - visit_bytes (borrowed): if input can be borrowed
// - visit_byte_buf (owned): if input must be copied
 
fn deserializer_choice<'de, D>(deserializer: D) -> Result<Cow<'de, [u8]>, D::Error>
where
    D: Deserializer<'de>,
{
    // The deserializer receives the request for bytes
    // It decides based on:
    // 1. Whether input is borrowed (can provide &'de [u8])
    // 2. Format constraints (e.g., base64 decoding required)
    // 3. Implementation details
    
    deserializer.deserialize_bytes(FlexibleBytesVisitor)
}
 
// Contrast with deserialize_str for strings:
// - visit_str: borrowed &str
// - visit_string: owned String
// Similar pattern, but for string data

deserialize_bytes tells the deserializer to call visit_bytes or visit_byte_buf.

Lifetime Considerations

use serde::Deserialize;
use std::borrow::Cow;
 
// The 'de lifetime ties borrowed data to input lifetime
#[derive(Deserialize)]
struct NetworkPacket<'de> {
    // Header is small, always parse to owned
    header: PacketHeader,
    
    // Payload borrows from input - zero-copy
    #[serde(borrow)]
    payload: &'de [u8],
}
 
#[derive(Deserialize)]
struct PacketHeader {
    version: u8,
    length: u32,
}
 
// For this to work:
// 1. Input must be borrowed (e.g., &[u8])
// 2. Input must live as long as the deserialized struct
// 3. Format must support borrowing bytes
 
fn process_packet<'de>(input: &'de [u8]) -> NetworkPacket<'de> {
    // Input lifetime 'de matches struct lifetime 'de
    bincode::deserialize(input).unwrap()
    // The returned struct borrows from input
    // input must remain valid for struct's lifetime
}

Borrowed byte data has lifetime constraints tied to the input source.

Common Pitfalls

use serde::de::{Deserialize, Deserializer, Visitor, Error};
use std::fmt;
 
// Pitfall 1: Expecting borrowed but receiving owned
struct BorrowedOnlyVisitor;
 
impl<'de> Visitor<'de> for BorrowedOnlyVisitor {
    type Value = &'de [u8];
 
    fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "borrowed bytes")
    }
 
    fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
    where
        E: Error,
    {
        Ok(v)
    }
 
    fn visit_byte_buf<E>(self, _v: Vec<u8>) -> Result<Self::Value, E>
    where
        E: Error,
    {
        // This can happen with formats that can't borrow
        Err(Error::custom("expected borrowed bytes but got owned bytes"))
    }
}
 
// Pitfall 2: Wrong lifetime on return type
struct WrongLifetimeVisitor;
 
impl<'de> Visitor<'de> for WrongLifetimeVisitor {
    type Value = Vec<u8>;  // Owned, so no 'de needed
 
    fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
    where
        E: Error,
    {
        // Must copy because return type is owned
        Ok(v.to_vec())
    }
 
    fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
    where
        E: Error,
    {
        Ok(v)
    }
}
 
// Pitfall 3: JSON expecting borrowed bytes
fn json_borrowed_bytes_fails() {
    let json = r#""base64encodeddata""#;
    
    // This will fail - JSON strings call visit_str, not visit_bytes
    // And even if handled, JSON can't provide borrowed bytes
    // let bytes: &[u8] = serde_json::from_str(json).unwrap();
    
    // JSON bytes typically use:
    let bytes: Vec<u8> = serde_json::from_str(json).unwrap();  // Owned
}

Understanding format limitations prevents runtime errors.

Synthesis

Quick reference:

Method Input Type Can Return Use Case
visit_bytes &[u8] &'de [u8] or Vec<u8> Zero-copy borrowing
visit_byte_buf Vec<u8> Vec<u8> or Cow::Owned Owned bytes only

When visit_bytes is called:

// Binary formats (bincode, postcard) with borrowed input:
// - Input is &[u8]
// - Byte fields can borrow from input
// - Calls visit_bytes with slice of input
 
// Self-describing formats (may or may not):
// - Depends on input ownership
// - May fall back to visit_byte_buf
 
// JSON:
// - Never calls visit_bytes for byte fields
// - Strings require decoding
// - Must use visit_str/visit_string or accept owned

Borrowed vs Owned pattern:

use serde::de::{Visitor, Error};
use std::borrow::Cow;
 
// Borrowed only: &'de [u8]
type BorrowedResult<'de> = &'de [u8];
 
// Owned only: Vec<u8>
type OwnedResult = Vec<u8>;
 
// Flexible: Cow<'de, [u8]>
type FlexibleResult<'de> = Cow<'de, [u8]>;
 
// For borrowed, visit_byte_buf must error
// For owned, both can succeed (copying from visit_bytes)
// For Cow, visit_bytes borrows, visit_byte_buf owns

Key insight: visit_bytes is the zero-copy pathway in serde's deserialization machinery. When a deserializer can provide borrowed bytes—meaning the input source outlives the deserialized value and the format doesn't require transformation—it calls visit_bytes with a slice borrowed from the input. The visitor can then return this borrowed slice directly, avoiding allocation. When borrowing isn't possible (input doesn't live long enough, format requires decoding, or data must be transformed), the deserializer calls visit_byte_buf with an owned Vec<u8>. The visitor implementation determines the return type: returning &'de [u8] requires visit_bytes (borrowed), returning Vec<u8> works with either method (owned), and returning Cow<'de, [u8]> enables flexible handling of both cases. Binary formats like bincode and postcard support visit_bytes when deserializing from a borrowed byte slice, while JSON cannot provide borrowed bytes due to string encoding requirements. For maximum compatibility, visitors should handle both visit_bytes and visit_byte_buf, using Cow<'de, [u8]> as the flexible return type that adapts to whatever the deserializer provides.