Rust in Everything
基于 serde 1.0.229

Content:缓冲的代价

主线位置:缓冲主线的中段。untagged、内部标签、flatten 生成的代码在「还不知道该交给哪个 Visitor」时,先用 ContentVisitor 把输入读成一棵 Content 树(读入),再让 ContentDeserializer 或 ContentRefDeserializer 假装成格式,对这棵树把反序列化主线再跑一遍(重放)。本页讲这棵树、读入和重放这两步,以及中间丢掉了什么、多花了多少。三种写法各自怎么调用它们,见 内部标签与相邻标签、untagged 和 flatten。

flowchart TB
  gen["生成的代码"] --> seed["ContentVisitor 作 seed"]
  seed --> hook["__deserialize_content_v1"]
  hook -->|默认| anyde["格式的 deserialize_any"]
  hook -->|"格式自己覆盖"| own["格式自定"]
  anyde --> tree["Content 树"]
  own --> tree
  tree --> owned["ContentDeserializer"]
  tree --> byref["ContentRefDeserializer"]
  owned --> replay["T::deserialize 再跑一遍"]
  byref --> replay
  replay -->|"deserialize_u8 等提示"| cb["按变体回调"]

本页的实验都在 M2 Max、macOS、rustc 1.99.0 上跑,依赖 serde = "=1.0.229"(derive)、serde_json = "=1.0.154"、bincode = "=1.3.3"、postcard = "1"(1.1.3)、serde_yaml = "=0.9.34";arbitrary_precision 的实验单独放在一个打开了这个 feature 的 crate 里(cargo 会合并 feature,不能和别的实验混在一起)。

Content 的定义:为什么在 serde_core

Content<'de> 是整个文件 serde_core/src/private/content.rs 的全部内容:

// Used from generated code to buffer the contents of the Deserializer when
// deserializing untagged enums and internally tagged enums.
//
// Not public API. Use serde-value instead.
//
// Obsoleted by format-specific buffer types (https://github.com/serde-rs/serde/pull/2912).
#[doc(hidden)]
pub enum Content<'de> {
    Bool(bool),

    U8(u8),
    U16(u16),
    U32(u32),
    U64(u64),

    I8(i8),
    I16(i16),
    I32(i32),
    I64(i64),

    F32(f32),
    F64(f64),

    Char(char),
    String(String),
    Str(&'de str),
    ByteBuf(Vec<u8>),
    Bytes(&'de [u8]),

    None,
    Some(Box<Content<'de>>),

    Unit,
    Newtype(Box<Content<'de>>),
    Seq(Vec<Content<'de>>),
    Map(Vec<(Content<'de>, Content<'de>)>),
}

22 个变体,对照数据模型的 29 种类型,少的和合并的是:

数据模型Content 里后果
i128、u128没有读不进来,也放不出去(本页后面实验)
unit structUnit名字丢了
newtype structNewtype名字丢了;JSON 根本不产生它,见下文 deserialize_newtype_struct
tuple、tuple struct、seq都是 Seq长度和名字丢了
map、struct都是 Map名字和字段表丢了
四种 enum 变体没有ContentVisitor::visit_enum 直接报错
strStr(&'de str) 和 String(String)能借的借,借不成的拥有,见 生命周期 'de 与借用
bytesBytes(&'de [u8]) 和 ByteBuf(Vec<u8>)同上

Map 是 Vec 而不是 HashMap:键的顺序保留下来,重复键也保留,键也可以不是字符串。

定义放在 serde_core,读写它的代码却都在 serde/src/private/de.rs。原因在 Deserializer trait 上一个隐藏方法的签名里(下一节):trait 定义在 serde_core,签名里出现的类型也只能来自 serde_core,见源码地图。除了这个签名,serde_core 里没有任何代码用到 Content;它甚至没有 Deserialize impl(Deserialize 和 Content 都在 serde_core,按孤儿规则 serde 写不了这个 impl),1.0.222(#2976,「Move Content's Deserialize impl from serde_core to serde」)之后,「把输入读成 Content」这件事由 serde 里的 ContentVisitor 作为 DeserializeSeed 来做。

ContentVisitor:读进来

ContentVisitor 有两个身份。作为 DeserializeSeed,它只做一件事:

    impl<'de> DeserializeSeed<'de> for ContentVisitor<'de> {
        type Value = Content<'de>;

        fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
        where
            D: Deserializer<'de>,
        {
            deserializer.__deserialize_content_v1(self)
        }
    }

作为 Visitor,它把每个 visit_* 原样变成一个变体:visit_u8(v) 得到 Content::U8(v),visit_borrowed_str(v) 得到 Content::Str(v),visit_str(v) 得到 Content::String(v.into())(拷贝一份),visit_string(v) 直接收下。复合类型递归,visit_map:

        fn visit_map<V>(self, mut visitor: V) -> Result<Self::Value, V::Error>
        where
            V: MapAccess<'de>,
        {
            let mut vec =
                Vec::<(Content, Content)>::with_capacity(
                    size_hint::cautious::<(Content, Content)>(visitor.size_hint()),
                );
            while let Some(kv) =
                tri!(visitor.next_entry_seed(ContentVisitor::new(), ContentVisitor::new()))
            {
                vec.push(kv);
            }
            Ok(Content::Map(vec))
        }

每个键、每个值都用一个新的 ContentVisitor 作 seed 去读,所以树里的每个节点都各经过一次那个隐藏方法。size_hint::cautious 防止恶意长度一次分配太多,见标准库类型的实现。

有三个 visit_* 读不进 Content:visit_i128、visit_u128 没有实现,落到默认实现报 invalid_type(这两个默认不转发,见 Deserialize、Deserializer 与 Visitor);visit_enum 是写出来的,但只返回一个错误:

        fn visit_enum<V>(self, _visitor: V) -> Result<Self::Value, V::Error>
        where
            V: EnumAccess<'de>,
        {
            Err(de::Error::custom(
                "untagged and internally tagged enums do not support enum input",
            ))
        }

JSON 的 deserialize_any 从不调 visit_enum,碰不到这一条。YAML 的 !Tag value 写法会:serde_yaml 的 deserialize_any 遇到 !A 1 这样带标签的值,调的是 visit_enum。src/bin/yaml_enum.rs:

use serde::Deserialize;

#[derive(Deserialize, Debug)]
enum E {
  A(u8),
}

#[derive(Deserialize, Debug)]
struct Inner {
  e: E,
}

#[derive(Deserialize, Debug)]
#[serde(tag = "t")]
enum Tagged {
  V(Inner),
}

fn main() {
  let yaml = "t: V\ne: !A 1\n";
  println!("plain  {:?}", serde_yaml::from_str::<Inner>(yaml).map_err(|e| e.to_string()));
  println!("tagged {:?}", serde_yaml::from_str::<Tagged>(yaml).map_err(|e| e.to_string()));
}
plain  Ok(Inner { e: A(1) })
tagged Err("e: untagged and internally tagged enums do not support enum input at line 2 column 4")

同一段 YAML,Inner 直接读没问题;包进内部标签的变体之后,e 的值要先进 Content,而 Content 里没有 enum。

__deserialize_content_v1:留给格式的钩子

ContentVisitor 作 seed 时调的是 Deserializer 上的 __deserialize_content_v1:

    // Not public API.
    #[cfg(all(not(no_serde_derive), any(feature = "std", feature = "alloc")))]
    #[doc(hidden)]
    fn __deserialize_content_v1<V>(self, visitor: V) -> Result<V::Value, Self::Error>
    where
        V: Visitor<'de, Value = crate::private::Content<'de>>,
    {
        self.deserialize_any(visitor)
    }

默认就是 deserialize_any:格式不知道调用方想要什么,只能照输入里实际是什么回调。no_serde_derive 是 serde_core/build.rs 在 rustc 低于 1.71(serde_derive 的最低版本)时设的 cfg,那时没有 derive,也就用不到它。

谁覆盖了它?在 serde 1.0.229 和 serde_json 1.0.154 的源码里搜 __deserialize_content,一共四处:上面的默认实现、ContentVisitor 的调用,和 private/de.rs 里两个重放用的 Deserializer。serde_json 没有覆盖,本机 cargo 缓存里的 bincode、postcard、serde_yaml、toml 也都没有。两个覆盖都很短,ContentDeserializer 的覆盖:

        fn __deserialize_content_v1<V>(self, visitor: V) -> Result<V::Value, Self::Error>
        where
            V: Visitor<'de, Value = Content<'de>>,
        {
            let _ = visitor;
            Ok(self.content)
        }

ContentRefDeserializer 的覆盖是 Ok(content_clone(self.content))。意思是:如果「格式」本身已经是一棵 Content(缓冲套着缓冲,比如内部标签的变体里又是一个 untagged enum),里层要缓冲时直接把这棵树交出去,不用再 deserialize_any 一遍、按节点重新建一棵。

下面给 02-core 里用过的打印调用的 LogDe 加上这个方法(只打印一行,然后转给被包的格式),看 serde_json 收到的是什么。src/log_de.rs 里新加的部分:

  // The hidden hook: log it, then let the wrapped format decide (serde_json keeps the default).
  fn __deserialize_content_v1<V>(self, visitor: V) -> Result<V::Value, D::Error>
  where
    V: Visitor<'de, Value = serde::__private229::de::Content<'de>>,
  {
    log(self.depth, "__deserialize_content_v1");
    self.inner.__deserialize_content_v1(LogV { inner: visitor, depth: self.depth + 1 })
  }

src/bin/hook.rs:

use exp_serde_content::log_de::LogDe;
use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct Inner {
  x: u8,
}

#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum Untagged {
  V(Inner),
}

fn main() {
  let input = r#"{"x":7}"#;
  println!("Inner:");
  let mut de = serde_json::Deserializer::from_str(input);
  println!("=> {:?}", Inner::deserialize(LogDe::new(&mut de)).unwrap());
  println!("Untagged:");
  let mut de = serde_json::Deserializer::from_str(input);
  println!("=> {:?}", Untagged::deserialize(LogDe::new(&mut de)).unwrap());
}
Inner:
deserialize_struct("Inner", ["x"])
  visit_map
    next_key_seed
      deserialize_identifier
        visit_borrowed_str("x")
    -> Some
    next_value_seed
      deserialize_u8
        visit_u64(7)
    next_key_seed
    -> None
=> Inner { x: 7 }
Untagged:
__deserialize_content_v1
  visit_map
    size_hint() = None
    next_entry_seed
      __deserialize_content_v1
        visit_borrowed_str("x")
      __deserialize_content_v1
        visit_u64(7)
    -> Some
    next_entry_seed
    -> None
=> V(Inner { x: 7 })
  • 直接读 Inner,serde_json 收到 deserialize_struct、deserialize_identifier、deserialize_u8 三个提示。
  • 读 Untagged,serde_json 只收到 __deserialize_content_v1,键和值各一次,默认实现在内部转成 deserialize_any(它没被 LogDe 包住,所以没打出来)。deserialize_u8 这个提示根本没到 serde_json:它在重放时发给了 ContentRefDeserializer。
  • 键值是成对读的(next_entry_seed),size_hint() 是 None:JSON 对象事先不知道有几个键。
📝NOTE
冷知识:这个钩子先是 __deserialize_content,拆 crate 时丢过一次

#2146(「Improve performance using nightly features」)报告:在优化一个项目的反序列化性能时发现,Content 经常是从一个 ContentDeserializer 再读出来的(「__deserializer is serde::__private::de::ContentDeserailizer in many cases」);用 nightly 的 specialization 让这种情况直接 Ok(deserializer.content) 之后,基准从「1,226,131 ns/iter」降到「621,480 ns/iter」。#2148(「Optimize deserialization of recursive buffered types」,首次发布于 1.0.133)在稳定版上做到了同样的事:给 Deserializer 加一个隐藏方法 __deserialize_content,默认 deserialize_any,ContentDeserializer 覆盖成直接返回自己的 content,ContentRefDeserializer 覆盖成 clone。1.0.133 的发布说明是:「Optimize deserialization of data structures that contain recursive use of flatten fields or tag or untagged enums (#2148)」。

1.0.220 把 trait 拆进 serde_core 的提交(#2608)删掉了这个方法,ContentVisitor 改回直接调 deserialize_any;当时 Content 还定义在 serde 的 private/de.rs 里。同一个版本里 #2968(「Restore __deserialize_content optimization」)把 Content 的定义搬进 serde_core/src/private,方法以 __deserialize_content_v1 的名字加回来;本页开头那句「Obsoleted by format-specific buffer types」的注释也是这次加上的。两次改动都在 1.0.220 里,没有一个发布版缺这个优化。

这个钩子在本页的分配实验里还会出现:内部标签套 untagged,第二层缓冲不多分配一次。

重放:两个 ContentDeserializer

读进来的树要交给真正的类型,T::deserialize 需要一个 Deserializer。private/de.rs 里有两个:

ContentDeserializerContentRefDeserializer
字段content: Content<'de>content: &'a Content<'de>,Copy
注释「Used when deserializing an internally tagged enum because the content will be used exactly once.」「Used when deserializing an untagged enum because the content may need to be used more than once.」
Content::String 交出去visit_string(v),移交所有权visit_str(v),对方要 String 就得再拷贝一次
谁在用内部标签;flatten 进 struct 时被认领的键值untagged(每个变体试一次);flatten 进 map 时

两者的 Error 都是类型参数 E,生成的代码填的是格式的错误类型(ContentRefDeserializer::<__D::Error>::new(&__content)),所以重放时产生的错误还是 serde_json::Error,只是没有了位置(后面讲)。

提示怎么映射到 Content

重放时 T::deserialize 照常发提示,ContentDeserializer 按手里的变体回调。整数的提示都进同一个函数,deserialize_integer:

        fn deserialize_integer<V>(self, visitor: V) -> Result<V::Value, E>
        where
            V: Visitor<'de>,
        {
            match self.content {
                Content::U8(v) => visitor.visit_u8(v),
                Content::U16(v) => visitor.visit_u16(v),
                Content::U32(v) => visitor.visit_u32(v),
                Content::U64(v) => visitor.visit_u64(v),
                Content::I8(v) => visitor.visit_i8(v),
                Content::I16(v) => visitor.visit_i16(v),
                Content::I32(v) => visitor.visit_i32(v),
                Content::I64(v) => visitor.visit_i64(v),
                _ => Err(self.invalid_type(&visitor)),
            }
        }

deserialize_i8 到 deserialize_u64 八个方法都是 self.deserialize_integer(visitor):提示是 u8,内容是 Content::U64(7),就调 visit_u64(7),由 u8 的 Visitor 自己做范围检查(见标准库类型的实现)。提示在这里只起一个作用:内容不是整数时报错。deserialize_float 多接受八种整数变体,所以 f32 字段能读 7;反过来 deserialize_integer 不接受 F64,u8 字段读 7.0 报类型错。

另外两处有讲究:

  • deserialize_option(两个 Deserializer 写法相同,注释在 ContentRefDeserializer 那份里):None 调 visit_none,Some 拆一层,Unit 调 visit_unit,其余一律 visit_some(self)。注释说这是为了 JSON 这类「do not encode an indication whether a value is optional」的格式:JSON 的 deserialize_any 从不调 visit_some,7 进来就是 Content::U64(7),Option<u8> 字段得能把它读成 Some(7)。null 进来是 Content::Unit(serde_json 的 deserialize_any 对 null 调 visit_unit),Option 的 Visitor 把 visit_unit 当 None。
  • deserialize_newtype_struct:同样的道理,不是 Content::Newtype 的内容也当成 newtype 的内层,visitor.visit_newtype_struct(self)。

deserialize_u128 和 deserialize_i128 两个都没写,用的是 trait 的默认实现 deserialize_u128:Err(Error::custom("u128 is not supported"))。

实验:同一个字段,三条路

用一组「探针」类型看提示和回调的对应:每个探针的 Deserialize 只发一个固定的提示,Visitor 把收到的 visit_* 和值记成字符串;出错也记下来,探针本身不失败,这样 untagged 不会因为某个字段出错而整体失败。src/lib.rs(删掉了重复的 visit_*):

pub struct Log;

impl<'de> Visitor<'de> for Log {
  type Value = String;
  fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
    f.write_str("anything")
  }
  // visit_bool ... visit_byte_buf: 都是 Ok(format!("{}({:?})", 方法名, v))
  fn visit_some<D: Deserializer<'de>>(self, d: D) -> Result<String, D::Error> {
    Ok(format!("visit_some({})", d.deserialize_any(Log)?))
  }
  // visit_unit、visit_none、visit_newtype_struct、visit_seq、visit_map 同理
}

macro_rules! probe {
  ($($ty:ident => $method:ident),*) => {
    $(
      pub struct $ty(pub String);
      // ... impl Debug:打印 self.0
      impl<'de> de::Deserialize<'de> for $ty {
        fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
          Ok($ty(match d.$method(Log) {
            Ok(s) => s,
            Err(e) => format!("Err({e})"),
          }))
        }
      }
    )*
  };
}

probe! {
  PU8 => deserialize_u8,
  PU128 => deserialize_u128,
  PStr => deserialize_str,
  PString => deserialize_string,
  PBytes => deserialize_bytes,
  POption => deserialize_option,
  PBool => deserialize_bool
  // ...
}

src/bin/hints.rs:同一个 Fields 直接读、作为内部标签的变体读、作为 untagged 的变体读:

#[derive(Deserialize, Debug)]
struct Fields {
  a: PU8,
  b: PU128,
  big: PU128,
  s: PStr,
  esc: PStr,
  owned: PString,
  bytes: PBytes,
  opt: POption,
  flag: PBool,
}

#[derive(Deserialize, Debug)]
#[serde(tag = "t")]
enum Tagged {
  V(Fields),
}

#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum Untagged {
  V(Fields),
}

const INPUT: &str = r#"{"t":"V","a":7,"b":7,"big":18446744073709551616,
  "s":"hi","esc":"a\nb","owned":"hi","bytes":"hi","opt":7,"flag":"true"}"#;

// show() 逐行打印九个字段
fn main() {
  show("direct (serde_json::Deserializer)", &serde_json::from_str::<Fields>(INPUT).unwrap());
  let Tagged::V(f) = serde_json::from_str::<Tagged>(INPUT).unwrap();
  show("tag = \"t\" (ContentDeserializer)", &f);
  let Untagged::V(f) = serde_json::from_str::<Untagged>(INPUT).unwrap();
  show("untagged (ContentRefDeserializer)", &f);
}
direct (serde_json::Deserializer)
  a     deserialize_u8      -> visit_u64(7)
  b     deserialize_u128    -> visit_u128(7)
  big   deserialize_u128    -> visit_u128(18446744073709551616)
  s     deserialize_str     -> visit_borrowed_str("hi")
  esc   deserialize_str     -> visit_str("a\nb")
  owned deserialize_string  -> visit_borrowed_str("hi")
  bytes deserialize_bytes   -> visit_borrowed_bytes([104, 105])
  opt   deserialize_option  -> visit_some(visit_u64(7))
  flag  deserialize_bool    -> Err(invalid type: string "true", expected anything at line 2 column 71)
tag = "t" (ContentDeserializer)
  a     deserialize_u8      -> visit_u64(7)
  b     deserialize_u128    -> Err(u128 is not supported)
  big   deserialize_u128    -> Err(u128 is not supported)
  s     deserialize_str     -> visit_borrowed_str("hi")
  esc   deserialize_str     -> visit_string("a\nb")
  owned deserialize_string  -> visit_borrowed_str("hi")
  bytes deserialize_bytes   -> visit_borrowed_str("hi")
  opt   deserialize_option  -> visit_some(visit_u64(7))
  flag  deserialize_bool    -> Err(invalid type: string "true", expected anything)
untagged (ContentRefDeserializer)
  a     deserialize_u8      -> visit_u64(7)
  b     deserialize_u128    -> Err(u128 is not supported)
  big   deserialize_u128    -> Err(u128 is not supported)
  s     deserialize_str     -> visit_borrowed_str("hi")
  esc   deserialize_str     -> visit_str("a\nb")
  owned deserialize_string  -> visit_borrowed_str("hi")
  bytes deserialize_bytes   -> visit_borrowed_str("hi")
  opt   deserialize_option  -> visit_some(visit_u64(7))
  flag  deserialize_bool    -> Err(invalid type: string "true", expected anything)

逐行看:

  • a:三条路一样。serde_json 对 deserialize_u8 本来也是解析出一个数再 visit_u64,缓冲没有改变什么。
  • b、big:直接读时 serde_json 的 deserialize_u128 自己解析数字,连超出 u64 的 18446744073709551616 也能读;缓冲之后两个都是 u128 is not supported,连 7 都不行。内容在 Content 里是什么(U64(7)、F64)无关紧要,ContentDeserializer 根本没有实现这个方法。
  • s:没有转义的字符串在 Content 里是 Str,重放时还能借。
  • esc:有转义,serde_json 只能 visit_str,进 Content 时拷贝成 String。重放时两条缓冲路分开了:拿所有权的移交(visit_string),借用的只能再给一个 &str(visit_str)。
  • bytes:直接读时 serde_json 按提示把字符串当字节给出(visit_borrowed_bytes);Content 里存的是 Str,重放时 deserialize_bytes 交出去的是 visit_borrowed_str。要字节的一方得自己也接受字符串。
  • opt:三条路一样,靠的是上面 deserialize_option 的「其余一律 visit_some」。
  • flag:报错一样,但直接读的那条带着 at line 2 column 71,缓冲的两条没有位置。

丢掉了什么

把上面的探针换成真实的类型,看用户会碰到什么。src/bin/losses.rs:

use std::collections::HashMap;

use serde::{Deserialize, Serialize};
use serde::de::DeserializeOwned;

#[derive(Serialize, Deserialize, Debug)]
struct Big {
  n: u128,
}

#[derive(Serialize, Deserialize, Debug)]
struct Keys {
  m: HashMap<u32, bool>,
}

#[derive(Serialize, Deserialize, Debug)]
struct Small {
  x: u8,
}

// The same struct three ways: as is, as an internally tagged variant, as an untagged variant.
macro_rules! wrap {
  ($inner:ident, $tagged:ident, $untagged:ident) => {
    #[derive(Serialize, Deserialize, Debug)]
    #[serde(tag = "t")]
    enum $tagged {
      V($inner),
    }
    #[derive(Serialize, Deserialize, Debug)]
    #[serde(untagged)]
    enum $untagged {
      V($inner),
    }
  };
}
wrap!(Big, BigTagged, BigUntagged);
wrap!(Keys, KeysTagged, KeysUntagged);
wrap!(Small, SmallTagged, SmallUntagged);

fn json<T: DeserializeOwned + std::fmt::Debug>(label: &str, s: &str) {
  match serde_json::from_str::<T>(s) {
    Ok(v) => println!("  {label:<9} Ok({v:?})"),
    Err(e) => println!("  {label:<9} Err({e})"),
  }
}

fn main() {
  let big = r#"{"t":"V","n":7}"#;
  println!("u128 field, input {big}");
  json::<Big>("plain", big);
  json::<BigTagged>("tag", big);
  json::<BigUntagged>("untagged", big);
  // ... Keys 用 {"t":"V","m":{"1":true}},Small 用 {"t":"V","x":300},同样三行

  println!("non-self-describing formats, value SmallTagged::V(Small {{ x: 7 }})");
  let v = SmallTagged::V(Small { x: 7 });
  let b = bincode::serialize(&v).unwrap();
  println!("  bincode  bytes {b:?}");
  println!("  bincode  back  {:?}", bincode::deserialize::<SmallTagged>(&b).map_err(|e| e.to_string()));
  let p = postcard::to_allocvec(&v).unwrap();
  println!("  postcard bytes {p:?}");
  println!("  postcard back  {:?}", postcard::from_bytes::<SmallTagged>(&p).map_err(|e| e.to_string()));
  let b = bincode::serialize(&SmallUntagged::V(Small { x: 7 })).unwrap();
  println!("  bincode  untagged back {:?}", bincode::deserialize::<SmallUntagged>(&b).map_err(|e| e.to_string()));
}
u128 field, input {"t":"V","n":7}
  plain     Ok(Big { n: 7 })
  tag       Err(u128 is not supported)
  untagged  Err(data did not match any variant of untagged enum BigUntagged)
HashMap<u32, bool> field, input {"t":"V","m":{"1":true}}
  plain     Ok(Keys { m: {1: true} })
  tag       Err(invalid type: string "1", expected u32)
  untagged  Err(data did not match any variant of untagged enum KeysUntagged)
u8 field, input {"t":"V","x":300}
  plain     Err(invalid value: integer `300`, expected u8 at line 1 column 16)
  tag       Err(invalid value: integer `300`, expected u8)
  untagged  Err(data did not match any variant of untagged enum SmallUntagged)
non-self-describing formats, value SmallTagged::V(Small { x: 7 })
  bincode  bytes [1, 0, 0, 0, 0, 0, 0, 0, 86, 7]
  bincode  back  Err("Bincode does not support the serde::Deserializer::deserialize_any method")
  postcard bytes [1, 86, 7]
  postcard back  Err("This is a feature that PostCard will never implement")
  bincode  untagged back Err("Bincode does not support the serde::Deserializer::deserialize_any method")

下面逐条说。untagged 那一列把每个变体的失败原因都吞成了一句话,为什么见 untagged;内部标签那一列能看到真正的原因。

u128 / i128。 上一节说过,读入(ContentVisitor 没有 visit_u128)和重放(ContentDeserializer 没有 deserialize_u128)两头都不支持。JSON 这里是重放那一头先失败:7 进 Content 时是 U64(7),读入没问题,u128 字段一要就报错。

格式按提示做的特殊处理。 HashMap<u32, bool> 直接读能行,是因为 serde_json 的 MapKey 收到 deserialize_u32 提示时走 deserialize_numeric_key!,把带引号的 "1" 当数字解析。缓冲时键是按 deserialize_any 读的,在 Content 里就是 Str("1"),重放时 deserialize_u32 对着一个字符串只能报 invalid type: string "1", expected u32。这正是 #1183(「Internal buffering disrupts format-specific deserialization features」,至今 open)里的例子。flatten 进一个数字键的 map 是同一个问题,见 flatten。

报错位置。 直接读时错误带 at line 1 column 16。serde_json 在 deserialize_struct、deserialize_any 等方法返回前对出错的结果调 fix_position,给没有位置的错误补上当前读到的位置(见错误)。内部标签和 untagged 的重放发生在 deserialize_any 已经返回之后,这时 serde_json 不在调用栈上,错误一路冒出去也没人补位置。

非自描述格式。 写出没问题(bincode 的 [1, 0, 0, 0, 0, 0, 0, 0, 86, 7] 是长度 1 的字符串 "V" 加一个字节 7),读回来必须 deserialize_any,bincode 和 postcard 都不支持(postcard 的报错是它的 WontImplement 错误)。为什么这两个格式答不了「这里是什么」,见自描述格式与非自描述格式。

arbitrary_precision:数字变成了 map

serde_json 打开 arbitrary_precision 后,Number 内部存的是原始的十进制字符串。为了把这段字符串经过数据模型交给 serde_json::Value,parse_any_number 只把放得进 u64 / i64 的整数照常给出,其余(小数、超出范围的整数、-0)都变成 ParserNumber::String,ParserNumber::visit 对它调 visitor.visit_map(NumberDeserializer { .. }):一个只有一个键的 map,键是魔法名字 $serde_json::private::Number(这套约定见 Value)。f64 的 Visitor 不认 map。直接读时没事,因为 serde_json 的 deserialize_f64 经 deserialize_number 调的是 parse_integer,不经过 parse_any_number,照常 visit_f64;缓冲时 deserialize_any 交出的就是这个 map,Content 原样存成 Map。

另一个只打开了 arbitrary_precision 和 raw_value 的实验 crate,src/bin/arbitrary.rs:

use serde::Deserialize;
use serde::de::DeserializeOwned;
use serde_json::value::RawValue;

#[derive(Deserialize, Debug)]
struct Num {
  n: u64,
  f: f64,
}

#[derive(Deserialize, Debug)]
#[serde(tag = "t")]
enum NumTagged {
  V(Num),
}

#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum NumUntagged {
  V(Num),
}

#[derive(Deserialize, Debug)]
struct Val {
  n: serde_json::Value,
}

#[derive(Deserialize, Debug)]
#[serde(tag = "t")]
enum ValTagged {
  V(Val),
}

#[derive(Deserialize, Debug)]
struct Raw {
  r: Box<RawValue>,
}

#[derive(Deserialize, Debug)]
#[serde(tag = "t")]
enum RawTagged {
  V(Raw),
}

// json() 同上
fn main() {
  let num = r#"{"t":"V","n":7,"f":1.5}"#;
  println!("input {num}");
  json::<Num>("plain", num);
  json::<NumTagged>("tag", num);
  json::<NumUntagged>("untagged", num);
  let val = r#"{"t":"V","n":3.14159265358979323846264338327950288}"#;
  println!("input {val}");
  json::<Val>("Value plain", val);
  json::<ValTagged>("Value tag", val);
  let raw = r#"{"t":"V","r":[1, 2]}"#;
  println!("input {raw}");
  json::<Raw>("RawValue plain", raw);
  json::<RawTagged>("RawValue tag", raw);
}
input {"t":"V","n":7,"f":1.5}
  plain          Ok(Num { n: 7, f: 1.5 })
  tag            Err(invalid type: map, expected f64)
  untagged       Err(data did not match any variant of untagged enum NumUntagged)
input {"t":"V","n":3.14159265358979323846264338327950288}
  Value plain    Ok(Val { n: Number(3.14159265358979323846264338327950288) })
  Value tag      Ok(V(Val { n: Number(3.14159265358979323846264338327950288) }))
input {"t":"V","r":[1, 2]}
  RawValue plain Ok(Raw { r: RawValue([1, 2]) })
  RawValue tag   Err(invalid type: newtype struct, expected any valid JSON value)
  • n: u64 读 7 没问题(放得进 u64,照常 visit_u64),f: f64 读 1.5 报 invalid type: map, expected f64。所以「打开 arbitrary_precision 后内部标签里的整数还能用、小数全坏」:只有一部分数字受影响,容易在测试里漏掉。
  • Value 不受影响:它的 Visitor 认得那个魔法键,从 Content::Map 重放出来照样还原成 Number,精度也在。
  • RawValue 坏掉的原因不同:它按魔法名字调 deserialize_newtype_struct,要求格式交出原始的 JSON 片段;缓冲之后原始文本已经没了,ContentDeserializer 只能把内容当成 newtype 的内层(visit_newtype_struct(self)),RawValue 的 Visitor 拒绝。

代价:多几次分配

缓冲的直接开销是:建一棵树(每个 Seq、Map 一个 Vec,String、ByteBuf 各一块),再遍历一次。下面用一个计数的全局分配器数一次 from_str 里的分配次数和字节数(realloc 也算一次),再在 release 下跑 20 万次取平均耗时。src/bin/cost.rs:

use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};
use std::time::Instant;

use serde::Deserialize;
use serde::de::DeserializeOwned;

// Counts every allocation (and its bytes) made by the process.
struct Counting;
static ALLOCS: AtomicUsize = AtomicUsize::new(0);
static BYTES: AtomicUsize = AtomicUsize::new(0);

unsafe impl GlobalAlloc for Counting {
  unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
    ALLOCS.fetch_add(1, Relaxed);
    BYTES.fetch_add(layout.size(), Relaxed);
    unsafe { System.alloc(layout) }
  }
  unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
    unsafe { System.dealloc(ptr, layout) }
  }
  unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
    ALLOCS.fetch_add(1, Relaxed);
    BYTES.fetch_add(new_size, Relaxed);
    unsafe { System.realloc(ptr, layout, new_size) }
  }
}

#[global_allocator]
static GLOBAL: Counting = Counting;

#[derive(Deserialize, Debug)]
struct Post {
  title: String,
  tags: Vec<String>,
  views: u64,
}

#[derive(Deserialize, Debug)]
#[serde(tag = "t")]
enum Tagged {
  Post(Post),
}

#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum Untagged {
  Post(Post),
}

#[derive(Deserialize, Debug)]
struct Flat {
  #[serde(flatten)]
  post: Post,
}

// Internally tagged around untagged: buffering inside buffering.
#[derive(Deserialize, Debug)]
#[serde(tag = "t")]
enum Outer {
  Post(UntaggedInner),
}

#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum UntaggedInner {
  Post(Post),
}

// Hides the hook: every method, including __deserialize_content_v1, ends in deserialize_any.
struct NoHook<D>(D);

impl<'de, D: serde::Deserializer<'de>> serde::Deserializer<'de> for NoHook<D> {
  type Error = D::Error;
  fn deserialize_any<V: serde::de::Visitor<'de>>(self, v: V) -> Result<V::Value, D::Error> {
    self.0.deserialize_any(v)
  }
  serde::forward_to_deserialize_any! {
    bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
    bytes byte_buf option unit unit_struct newtype_struct seq tuple
    tuple_struct map struct enum identifier ignored_any
  }
}

#[derive(Debug)]
struct HiddenInner(UntaggedInner);

impl<'de> Deserialize<'de> for HiddenInner {
  fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
    UntaggedInner::deserialize(NoHook(d)).map(HiddenInner)
  }
}

#[derive(Deserialize, Debug)]
#[serde(tag = "t")]
enum OuterNoHook {
  Post(HiddenInner),
}

fn measure<T: DeserializeOwned>(label: &str, input: &str) {
  let a0 = ALLOCS.load(Relaxed);
  let b0 = BYTES.load(Relaxed);
  let v: T = serde_json::from_str(input).unwrap();
  let allocs = ALLOCS.load(Relaxed) - a0;
  let bytes = BYTES.load(Relaxed) - b0;
  drop(v);
  const N: u32 = 200_000;
  let t = Instant::now();
  for _ in 0..N {
    let v: T = serde_json::from_str(std::hint::black_box(input)).unwrap();
    std::hint::black_box(v);
  }
  let ns = t.elapsed().as_nanos() / N as u128;
  println!("{label:<22} allocs={allocs:<3} bytes={bytes:<5} {ns} ns");
}

fn main() {
  println!("size_of::<Content>() = {}", std::mem::size_of::<serde::__private229::de::Content>());
  let plain = r#"{"t":"Post","title":"hello","tags":["a","b","c"],"views":7}"#;
  let escaped = r#"{"t":"Post","title":"he\"llo","tags":["a","b","c"],"views":7}"#;
  for (name, input) in [("no escapes", plain), ("escaped strings", escaped)] {
    println!("{name}: {input}");
    measure::<Post>("plain struct", input);
    measure::<Tagged>("tag = \"t\"", input);
    measure::<Untagged>("untagged", input);
    measure::<Flat>("flatten", input);
    measure::<Outer>("tag wrapping untagged", input);
    measure::<OuterNoHook>("same, hook hidden", input);
  }
}

Post 直接读时 "t" 是未知字段,被忽略。最后一行用 NoHook 把里层面对的 ContentDeserializer 包起来:forward_to_deserialize_any! 生成的方法都转给 deserialize_any,__deserialize_content_v1 没有覆盖,落到默认实现,也是 deserialize_any。这样里层看不到钩子,用来对比。M2 Max、rustc 1.99.0,cargo run --release,跑了三次,分配那两列每次相同,耗时每次相差不到 15 ns,下面是第一次的前半段(后半段在文末):

size_of::<Content>() = 32
no escapes: {"t":"Post","title":"hello","tags":["a","b","c"],"views":7}
plain struct           allocs=5   bytes=104   169 ns
tag = "t"              allocs=7   bytes=464   284 ns
untagged               allocs=7   bytes=464   283 ns
flatten                allocs=7   bytes=464   275 ns
tag wrapping untagged  allocs=7   bytes=464   268 ns
same, hook hidden      allocs=8   bytes=656   311 ns
❗IMPORTANT
动脑筋:转义输入下,untagged 分配几次?

还是上面的 cost.rs,第二段输入只把 title 换成了带转义的 "he\"llo"。这一段里 plain struct 是 6 次、tag = "t" 是 8 次。untagged 是几次?tag wrapping untagged 呢?先猜,再看文末。

第一段输入(没有转义)的账:

  • 直接读,5 次、104 字节。 title 的 String(5 字节)、三个 tag 的 String(各 1 字节)、tags 的 Vec<String>(serde_json 的数组没有 size_hint,Vec 第一次 push 时按最小容量 4 分配,4 × 24 = 96 字节)。
  • 缓冲,多 2 次、多 360 字节。 Content 是 32 字节,一对键值 64 字节。整个对象的 Content::Map 一个 Vec(untagged 是 4 个键,内部标签去掉标签剩 3 个,容量都是 4,256 字节),tags 的 Content::Seq 一个 Vec(容量 4,128 字节)。字符串都没有转义,在树里是 Str,借用输入,不分配。重放时 Vec<String> 拿到的 size_hint 是精确的 3(SeqRefDeserializer 知道还剩几个),按 3 预分配,比直接读还省了 24 字节:104 + 256 + 128 − 24 = 464。
  • 耗时多六成多。 这里的输入很小,建树、遍历、多出来的两次分配占的比例就大。
  • 内部标签套 untagged 和单独一层一样,7 次。 外层 TaggedContentVisitor 建了一棵树;里层 untagged 用 ContentVisitor 再要一棵时,面对的「格式」是 ContentDeserializer,它的 __deserialize_content_v1 直接把自己的树交出去。
  • 把钩子藏起来,多 1 次、多 192 字节。 里层对 NoHook 调 deserialize_any,ContentDeserializer 按 visit_map 把顶层重新喂给 ContentVisitor,重建顶层 Map 的 Vec:3 项(标签已被外层拿走),这次 size_hint 是精确的 3,3 × 64 = 192 字节。只重建了这一层:每个值仍由 ContentDeserializer 交给新的 ContentVisitor,那里钩子又生效了,tags 的 Seq 原样交出。钩子省下的是每一层「格式本身就是 Content」时的重建。

「Obsoleted by #2912」

Content 定义上方那句「Obsoleted by format-specific buffer types (https://github.com/serde-rs/serde/pull/2912)」读起来像是已经被取代了,其实没有。#2912(「Customizable buffer type (proof of concept)」)在 2025-03-22 创建,到本文写作时(2026-10)还是一个未合并的草稿 PR。它的说明是:

This demonstrates a backward compatible way that a format such as serde_json could use its own serde_json::Value as its buffer type for untagged enums and flatten, instead of "Content", solving https://github.com/serde-rs/serde/issues/1183. Uses "return position impl trait in traits" (stabilized in Rust 1.75 by https://github.com/rust-lang/rust/pull/115822) and "precise capturing in traits" (stabilized in Rust 1.87 by https://github.com/rust-lang/rust/pull/138128).

草稿里的设计:Deserializer 加一个带默认实现的 deserialize_buffer(self) -> Result<impl Buffer<'de, Error = Self::Error>, Self::Error>,MapAccess 加一个 next_value_buffer;新 trait Buffer<'de> 有两个方法 owned_deserializer 和 ref_deserializer,正好对应今天的 ContentDeserializer 和 ContentRefDeserializer。默认实现仍然读成 Content,所以不改的格式行为不变;serde_json 可以返回自己的 Value,用它自己的 Deserializer 重放,MapKey 那种按提示解析数字键的行为就能保留下来。草稿里改了 untagged 和相邻标签两处生成代码,改成调这几个方法,不再直接写 Content。

1.0.229 里能看到的只是这个方向留下的两处痕迹:那句注释,和 __deserialize_content_v1 这个钩子。技术上格式可以覆盖后者,自己建一棵 Content 交出去(它是 #[doc(hidden)],注释写着「Not public API.」,前面查过的格式都没这么做),但交出去的仍然只能是 Content,重放的仍然是 serde 的两个 ContentDeserializer,上一节丢掉的东西一样都回不来。

📝NOTE
没有蠢问题

问: serde 为什么不干脆用 serde_json::Value 这样现成的东西当缓冲,而要自己造一个 Content?

答: serde 不能依赖任何一个格式,缓冲类型只能在 serde 自己里面。而且它得装得下所有格式通过 deserialize_any 交出来的东西:Value 没有字节串、没有借用的 &'de str、分不清 u8 和 u64、没有 char,别的格式交出来的这些信息放进去就走样了。Content 就是「数据模型能表达的、deserialize_any 会给出的」那个最小公倍数。代价是它对每个格式都不够好:对 JSON 来说它不知道数字键、原始文本和任意精度。#2912 的思路就是反过来,让每个格式提供自己的缓冲类型。

动脑筋答案

cost.rs 第二段的完整输出:

escaped strings: {"t":"Post","title":"he\"llo","tags":["a","b","c"],"views":7}
plain struct           allocs=6   bytes=113   186 ns
tag = "t"              allocs=8   bytes=473   304 ns
untagged               allocs=9   bytes=479   323 ns
flatten                allocs=8   bytes=473   294 ns
tag wrapping untagged  allocs=9   bytes=479   303 ns
same, hook hidden      allocs=10  bytes=671   345 ns
  • plain struct 比第一段多 1 次:serde_json 第一次遇到转义时给它的 scratch 缓冲分配空间("he\"llo" 解码后 6 字节,写进 scratch,再拷贝成 String),字节数 104 → 113 多出的 9 字节就是 scratch 的容量 8 加上 String 长 5 变成 6 的那 1 字节。
  • tag = "t" 是 8 次:scratch 同样多 1 次;解码后的字符串进 Content 时 visit_str 拷贝成 Content::String,这一次顶替了直接读时 String 字段的那一次;重放时 ContentDeserializer 用 visit_string 把它整个移交给 title,不再分配。
  • untagged 是 9 次:前面都一样,但重放用的是 ContentRefDeserializer,它只借着这棵树,对 Content::String 只能 visit_str,title 的 String 要再拷贝一份,多 1 次、多 6 字节(473 → 479)。这就是上面那张表里「visit_string 移交」和「visit_str 再拷贝」的差别。
  • tag wrapping untagged 也是 9 次,和单独的 untagged 一样:外层的树由钩子原样交给里层,里层照 untagged 的方式借用重放。
  • same, hook hidden 是 10 次,比上一行多的 1 次和 192 字节,与第一段输入里一样,是重建顶层 Map 的那个 Vec。

小结

  • Content<'de> 是 22 个变体的缓冲树:没有 u128 / i128、没有 enum,struct、map 都成了 Map(Vec 存键值对),能借的字符串存成 Str。定义在 serde_core,只因为 Deserializer 的隐藏方法签名要用它;读写它的代码都在 serde/src/private/de.rs。
  • 读入:ContentVisitor 作 seed 调 __deserialize_content_v1,默认是 deserialize_any。serde_json 1.0.154 没有覆盖,覆盖它的只有 serde 自己的两个 ContentDeserializer(直接交出树或克隆),让缓冲套缓冲时不重建(#2148,1.0.133;拆 crate 时经 #2968 恢复,1.0.220)。
  • 重放:ContentDeserializer 拿所有权用一次(内部标签、flatten 进 struct),ContentRefDeserializer 借用可以试多次(untagged、flatten 进 map),后者对 String 只能 visit_str,多一次拷贝。整数提示都映射到 deserialize_integer,按实际变体回调;deserialize_option 把非空内容都当 Some。
  • 丢掉的东西都有真实报错:u128 is not supported;数字键 invalid type: string "1", expected u32(#1183);arbitrary_precision 下小数 invalid type: map, expected f64;RawValue 和 YAML 的 enum 标签;报错没有行列号;bincode、postcard 不支持 deserialize_any。
  • 代价:这个小例子里多 2 次分配、多 360 字节、耗时多六成多;带转义的字符串在 untagged 下再多一次拷贝。
  • 「Obsoleted by #2912」指的是一个未合并的草稿 PR:让格式用自己的缓冲类型(如 serde_json::Value)代替 Content。1.0.229 里它还没有发生。

💬 关联讨论

发起讨论
加载中...