What is the difference between serde::de::Visitor::visit_str and visit_string for zero-copy vs owned string deserialization?

visit_str receives a borrowed &str that enables zero-copy deserialization by borrowing directly from the input data, while visit_string receives an owned String that must be allocated—deserializers call visit_str when they can lend the string data, and visit_string when they have an owned allocation to transfer. The Visitor trait provides both methods because deserializers may have either borrowed or owned data depending on the format and source: a &str deserializer can call visit_str for true zero-copy, while a String or JSON parser that builds strings must call visit_string (or offer both via visit_borrowed_str and visit_string). The default visit_string implementation delegates to visit_str, but specialized implementations can handle owned data more efficiently.

The Visitor Trait String Methods

use serde::de::{Visitor, Error};
 
// Simplified view of the Visitor trait
trait Visitor<'de> {
    // Called when deserializer has a borrowed string
    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
    where
        E: Error,
    {
        // Default: create owned String, call visit_string
        self.visit_string(v.to_owned())
    }
    
    // Called when deserializer has an owned string
    fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
    where
        E: Error,
    {
        // Default: borrow, call visit_str
        self.visit_str(&v)
    }
    
    // Called when deserializer has a borrowed string with known lifetime
    fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
    where
        E: Error,
    {
        // Default: delegate to visit_str
        self.visit_str(v)
    }
}

The default implementations delegate between each other, but efficient implementations handle each case appropriately.

Zero-Copy Deserialization with visit_str

use serde::de::{Deserialize, Deserializer, Visitor, Error};
use std::marker::PhantomData;
 
// A struct that borrows string data
#[derive(Debug)]
struct BorrowedName<'a> {
    name: &'a str,
}
 
impl<'de> Deserialize<'de> for BorrowedName<'de> {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct BorrowedNameVisitor;
 
        impl<'de> Visitor<'de> for BorrowedNameVisitor {
            type Value = BorrowedName<'de>;
            
            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                f.write_str("a string")
            }
            
            // Key: handle borrowed strings directly
            fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
            where
                E: Error,
            {
                // Zero-copy: v is borrowed directly from input
                Ok(BorrowedName { name: v })
            }
            
            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
            where
                E: Error,
            {
                // May fail if deserializer can't provide borrowed data
                Err(Error::custom("expected borrowed string"))
            }
            
            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
            where
                E: Error,
            {
                // Can't return borrowed reference to owned data
                Err(Error::custom("expected borrowed string, got owned"))
            }
        }
        
        deserializer.deserialize_str(BorrowedNameVisitor)
    }
}

visit_borrowed_str enables zero-copy by accepting a string with the deserializer's lifetime.

Owned Deserialization with visit_string

use serde::de::{Deserialize, Deserializer, Visitor, Error};
 
// A struct that owns its string
#[derive(Debug)]
struct OwnedName {
    name: String,
}
 
impl<'de> Deserialize<'de> for OwnedName {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct OwnedNameVisitor;
 
        impl<'de> Visitor<'de> for OwnedNameVisitor {
            type Value = OwnedName;
            
            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                f.write_str("a string")
            }
            
            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
            where
                E: Error,
            {
                // Have to allocate: copy borrowed data
                Ok(OwnedName { name: v.to_owned() })
            }
            
            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
            where
                E: Error,
            {
                // Efficient: take ownership, no allocation
                Ok(OwnedName { name: v })
            }
        }
        
        deserializer.deserialize_str(OwnedNameVisitor)
    }
}

visit_string receives an already-allocated String, avoiding extra allocation.

When Deserializers Call Each Method

use serde::de::{Deserializer, IntoDeserializer};
use serde_json;
 
fn when_called() {
    // &str deserializer: calls visit_str (or visit_borrowed_str)
    let s = "hello";
    let deserializer = s.into_deserializer();
    // Provides borrowed data, can enable zero-copy
    
    // String deserializer: calls visit_string
    let s = String::from("hello");
    let deserializer = s.into_deserializer();
    // Has owned data, calls visit_string
    
    // JSON deserializer: depends on content
    let json = "\"hello\"";
    let deserializer = serde_json::Deserializer::from_str(json);
    // Calls visit_borrowed_str if possible (borrowing from input)
    
    // JSON from reader: always calls visit_string
    let json = b"\"hello\"".as_slice();
    let deserializer = serde_json::Deserializer::from_reader(json);
    // Must allocate, calls visit_string
}

The format and source determine which method the deserializer calls.

The Default Delegation Chain

use serde::de::Visitor;
 
fn default_chain() {
    // Default implementations (simplified):
    
    // 1. visit_borrowed_str -> visit_str
    // If deserializer has 'de lifetime data, borrow it
    
    // 2. visit_str -> visit_string (or vice versa)
    // Some defaults go str -> String (allocate)
    // Some defaults go String -> &str (borrow)
    
    // For Visitor<'de>:
    // visit_borrowed_str('de str) -> visit_str(&str) -> visit_string(String)
    //                          borrow       allocate
    
    // The chain ensures all methods work, but may allocate unnecessarily
    
    // Optimal implementation:
    // - For borrowed data: implement visit_borrowed_str
    // - For owned data: implement visit_string
    // - visit_str can delegate to either depending on needs
}

Understanding the delegation chain helps write efficient visitors.

Zero-Copy Requirements

use serde::de::{Deserialize, Deserializer, Visitor, Error};
 
// Zero-copy requires:
// 1. Visitor that accepts borrowed data
// 2. Deserializer that can provide borrowed data
// 3. Input that lives long enough
 
fn zero_copy_requirements() {
    // This works: input lives long enough
    fn from_str_zero_copy(input: &'static str) -> BorrowedName<'static> {
        serde_json::from_str(input).unwrap()
    }
    
    // This also works: input lives for deserialization
    fn from_str_borrowed<'a>(input: &'a str) -> BorrowedName<'a> {
        // &'a str can provide BorrowedName<'a>
        let deserializer = input.into_deserializer();
        BorrowedName::deserialize(deserializer).unwrap()
    }
    
    // This FAILS: reader doesn't provide borrowed strings
    fn from_reader_zero_copy() -> BorrowedName<'static> {
        // let reader = std::io::empty();
        // serde_json::from_reader(reader).unwrap()
        // Error: reader-based JSON can't provide borrowed strings
        unimplemented!()
    }
}

Zero-copy requires both visitor support and deserializer capability.

Borrowed vs Owned in JSON

use serde::Deserialize;
 
#[derive(Debug, Deserialize)]
struct Document<'a> {
    #[serde(borrow)]
    title: &'a str,  // Borrows from input
    author: String,   // Owned, copied
}
 
fn json_borrowing() {
    // from_str can provide borrowed strings
    let json = r#"{"title":"My Title","author":"Author"}"#;
    let doc: Document = serde_json::from_str(json).unwrap();
    
    // doc.title borrows from the json string
    // doc.author is an owned String
    
    println!("Title: {}", doc.title);  // Borrowed
    println!("Author: {}", doc.author); // Owned
    
    // from_reader cannot provide borrowed strings
    let json = r#"{"title":"My Title","author":"Author"}"#;
    let reader = json.as_bytes();
    let result: Result<Document, _> = serde_json::from_reader(reader);
    // May fail or allocate for borrowed fields
}

serde(borrow) attribute enables zero-copy for specific fields.

Handling Both Cases Efficiently

use serde::de::{Deserialize, Deserializer, Visitor, Error};
 
#[derive(Debug)]
enum MaybeOwned<'a> {
    Borrowed(&'a str),
    Owned(String),
}
 
impl<'de> Deserialize<'de> for MaybeOwned<'de> {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct MaybeOwnedVisitor;
 
        impl<'de> Visitor<'de> for MaybeOwnedVisitor {
            type Value = MaybeOwned<'de>;
            
            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                f.write_str("a string")
            }
            
            // Zero-copy: borrow from input
            fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
            where
                E: Error,
            {
                Ok(MaybeOwned::Borrowed(v))
            }
            
            // Owned: take the allocation
            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
            where
                E: Error,
            {
                Ok(MaybeOwned::Owned(v))
            }
            
            // Borrowed with unknown lifetime: must copy
            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
            where
                E: Error,
            {
                Ok(MaybeOwned::Owned(v.to_owned()))
            }
        }
        
        deserializer.deserialize_str(MaybeOwnedVisitor)
    }
}

A flexible visitor can handle both zero-copy and owned cases.

The visit_borrowed_str Distinction

use serde::de::{Deserializer, Visitor, Error};
 
fn borrowed_str_vs_str() {
    // visit_str(&str):
    // - Receives a borrow with SOME lifetime
    // - Lifetime may not be 'de
    // - May borrow from temporary within deserializer
    // - Cannot store in 'de-lifetime field
    
    // visit_borrowed_str(&'de str):
    // - Receives a borrow with EXACTLY 'de lifetime
    // - Lifetime ties to deserializer input
    // - Can store in 'de-lifetime field
    // - Enables true zero-copy
    
    // Example:
    struct FlexVisitor;
    
    impl<'de> Visitor<'de> for FlexVisitor {
        type Value = &'de str;
        
        fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
            f.write_str("a string")
        }
        
        fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
        where
            E: Error,
        {
            // Can return v directly: it has 'de lifetime
            Ok(v)
        }
        
        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
        where
            E: Error,
        {
            // Cannot return v: lifetime may not be 'de
            // v only lives for this function call
            Err(Error::custom("expected borrowed string with 'de lifetime"))
        }
        
        fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
        where
            E: Error,
        {
            // Cannot return &v: it's owned and local
            Err(Error::custom("expected borrowed string"))
        }
    }
}

visit_borrowed_str is the only method that can return 'de-lifetime references.

Cow for Automatic Selection

use serde::Deserialize;
use std::borrow::Cow;
 
#[derive(Debug, Deserialize)]
struct FlexibleName<'a> {
    // Cow automatically uses borrowed when possible, owned otherwise
    name: Cow<'a, str>,
}
 
fn cow_example() {
    // Zero-copy when available
    let json = r#"{"name":"borrowed"}"#;
    let doc1: FlexibleName = serde_json::from_str(json).unwrap();
    match doc1.name {
        Cow::Borrowed(s) => println!("Zero-copy: {}", s),
        Cow::Owned(s) => println!("Owned: {}", s),
    }
    
    // Owned when necessary
    let json = String::from(r#"{"name":"owned"}"#);
    let doc2: FlexibleName = serde_json::from_str(&json).unwrap();
    // May still be borrowed if deserializer supports it
}

Cow<'de, str> automatically handles both zero-copy and owned cases.

Performance Implications

use serde::de::Visitor;
 
fn performance_comparison() {
    // Zero-copy (visit_borrowed_str):
    // - No allocation
    // - No copying
    // - O(1) string "creation"
    // - Limited to input lifetime
    
    // Owned via visit_str:
    // - Allocates String
    // - Copies all bytes
    // - O(n) for n-byte string
    // - Can outlive input
    
    // Owned via visit_string:
    // - No allocation (already allocated)
    // - No copying (take ownership)
    // - O(1) string transfer
    // - Can outlive input
    
    // Performance ranking (for large strings):
    // 1. visit_borrowed_str: zero-copy, fastest
    // 2. visit_string: take ownership, no copy
    // 3. visit_str -> allocate: copy + allocate, slowest
}

Avoiding allocation via zero-copy or ownership transfer is significantly faster.

Custom Deserializer Behavior

use serde::de::{Deserializer, Visitor, Error};
use std::marker::PhantomData;
 
// A deserializer that always provides owned strings
struct OwnedStringDeserializer {
    input: String,
}
 
impl<'de> Deserializer<'de> for OwnedStringDeserializer {
    type Error = serde::de::value::Error;
    
    fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
    where
        V: Visitor<'de>,
    {
        // We have owned data, call visit_string
        visitor.visit_string(self.input)
    }
    
    // Other methods...
    fn deserialize_any<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
    where
        V: Visitor<'de>,
    {
        Err(Error::custom("only strings supported"))
    }
    
    // ... rest of trait methods
}
 
// A deserializer that can provide borrowed strings
struct BorrowedStringDeserializer<'a> {
    input: &'a str,
}
 
impl<'de> Deserializer<'de> for BorrowedStringDeserializer<'de> {
    type Error = serde::de::value::Error;
    
    fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
    where
        V: Visitor<'de>,
    {
        // We have borrowed data with 'de lifetime, call visit_borrowed_str
        visitor.visit_borrowed_str(self.input)
    }
    
    fn deserialize_any<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
    where
        V: Visitor<'de>,
    {
        Err(Error::custom("only strings supported"))
    }
}

Custom deserializers choose which visitor method to call based on their data.

Deserializing Into String

use serde::de::{Deserialize, Deserializer, Visitor, Error};
 
// A type that always wants owned strings
struct AlwaysOwned(String);
 
impl<'de> Deserialize<'de> for AlwaysOwned {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct AlwaysOwnedVisitor;
        
        impl<'de> Visitor<'de> for AlwaysOwnedVisitor {
            type Value = AlwaysOwned;
            
            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                f.write_str("a string")
            }
            
            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
            where
                E: Error,
            {
                // Copy borrowed data
                Ok(AlwaysOwned(v.to_owned()))
            }
            
            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
            where
                E: Error,
            {
                // Take ownership directly, more efficient
                Ok(AlwaysOwned(v))
            }
            
            fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
            where
                E: Error,
            {
                // Still need to copy for owned result
                Ok(AlwaysOwned(v.to_owned()))
            }
        }
        
        deserializer.deserialize_str(AlwaysOwnedVisitor)
    }
}

Even when wanting owned data, visit_string avoids extra allocation.

String Implementation Pattern

use serde::de::{Deserialize, Deserializer, Visitor};
 
// Built-in String Deserialize implementation (simplified)
impl<'de> Deserialize<'de> for String {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct StringVisitor;
        
        impl<'de> Visitor<'de> for StringVisitor {
            type Value = String;
            
            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                f.write_str("a string")
            }
            
            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                Ok(v.to_owned())  // Allocate
            }
            
            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                Ok(v)  // Take ownership, no copy
            }
            
            fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                Ok(v.to_owned())  // Allocate for borrowed
            }
        }
        
        deserializer.deserialize_str(StringVisitor)
    }
}

String's implementation takes ownership in visit_string but allocates in visit_str.

Synthesis

Method call chain:

// Deserializer decides which method to call based on its data:
// 
// Has 'de-lifetime borrowed string -> visit_borrowed_str
// Has owned String                -> visit_string
// Has temporary &str              -> visit_str
//
// Default implementations delegate:
// visit_borrowed_str -> visit_str -> visit_string (or reverse)

When each is called:

// visit_borrowed_str:
// - Deserializer has &str with 'de lifetime
// - Input lives for entire deserialization
// - Can enable zero-copy
// - Example: serde_json::from_str with borrowed input
 
// visit_string:
// - Deserializer has owned String
// - Must allocate anyway
// - Transfer ownership to visitor
// - Example: serde_json::from_reader (must allocate)
 
// visit_str:
// - Deserializer has &str with unknown lifetime
// - Cannot guarantee lifetime
// - Visitor may need to copy
// - Example: parsing intermediate representation

Efficiency ranking:

// For borrowed data (want to keep borrowed):
// 1. visit_borrowed_str: true zero-copy, O(1)
// 2. visit_str + copy: must allocate, O(n)
// 3. visit_string: not applicable (not called for borrowed)
 
// For owned data (want String):
// 1. visit_string: take ownership, O(1)
// 2. visit_str + copy: allocate and copy, O(n)
// 3. visit_borrowed_str + copy: allocate and copy, O(n)

Key insight: visit_str and visit_string represent two different paths in Serde's deserialization model—visit_str handles borrowed string data that may enable zero-copy, while visit_string handles owned strings that already have allocation. The visit_borrowed_str method is the true zero-copy path, called when the deserializer can guarantee the 'de lifetime. Efficient deserializers call visit_borrowed_str when they have borrowable input, visit_string when they have owned strings, and visitors implement all three to handle each case optimally: visit_borrowed_str returns borrowed data directly, visit_string takes ownership, and visit_str allocates only when necessary. This tripartite design allows Serde to express zero-copy deserialization while gracefully falling back to owned allocation when the input or format doesn't support borrowing.