Loading pageā¦
Rust walkthroughs
Loading pageā¦
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 representationEfficiency 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.