Rust in Everything
基于 serde 1.0.229

flatten

主线位置:两条主线上各有一段,反序列化那一段属于缓冲主线。序列化时,带 #[serde(flatten)] 字段的结构体不再调 serialize_struct,而是 serialize_map(None),被展开的字段经 FlatMapSerializer 把自己的键值写进同一个 map。反序列化时,生成的 visit_map 把所有没认出的键值缓冲成 Content,循环结束后再用 FlatMapDeserializer 把这份缓冲依次交给每个 flatten 字段。Content 本身和它丢掉的东西见 Content:缓冲的代价。

flowchart TB
  vm["visit_map 循环"] --> key{"键认得吗"}
  key -->|普通字段| direct["next_value 直接读"]
  key -->|不认得| collect["值读成 Content 进 __collect"]
  collect --> after["循环结束"]
  direct --> after
  after --> fmd["FlatMapDeserializer"]
  fmd -->|deserialize_struct| take["认领键值并删掉"]
  fmd -->|"deserialize_map 或 any"| borrow["只借用,不删"]
  fmd -->|deserialize_option| opt["失败就当 None"]
  take --> deny{"deny_unknown_fields"}
  borrow --> deny
  deny -->|"还有剩下的键"| err["unknown field"]

本页的实验都在 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)。

序列化:一个 map,几个 FlatMapSerializer

derive 生成结构体的 Serialize 时只看一件事(has_non_skipped_flatten):有没有没被 skip_serializing 的 flatten 字段。有就走 serialize_struct_as_map。下面这个结构体:

#[derive(Serialize)]
struct Meta {
  author: String,
  views: u32,
}

#[derive(Serialize)]
struct Post {
  title: String,
  #[serde(flatten)]
  meta: Meta,
  #[serde(flatten)]
  extra: BTreeMap<String, u32>,
}

cargo expand 出来的 Serialize:

            let mut __serde_state = _serde::Serializer::serialize_map(
                __serializer,
                _serde::__private229::None,
            )?;
            _serde::ser::SerializeMap::serialize_entry(
                &mut __serde_state,
                "title",
                &self.title,
            )?;
            _serde::Serialize::serialize(
                &&self.meta,
                _serde::__private229::ser::FlatMapSerializer(&mut __serde_state),
            )?;
            _serde::Serialize::serialize(
                &&self.extra,
                _serde::__private229::ser::FlatMapSerializer(&mut __serde_state),
            )?;
            _serde::ser::SerializeMap::end(__serde_state)

普通字段 serialize_entry,flatten 字段把外层的 map 状态借给一个 FlatMapSerializer,让 Meta 和 BTreeMap 以为自己在面对一个格式:

pub struct FlatMapSerializer<'a, M: 'a>(pub &'a mut M);

它是一个 Serializer,Ok = ():内层序列化的结果不是一个值,而是往外层 map 里写了几个条目。内层是 struct 时,serialize_struct 返回 FlatMapSerializeStruct,它把每个字段改写成外层的一个条目,end 什么也不做(外层 map 由外层自己 end):

impl<'a, M> ser::SerializeStruct for FlatMapSerializeStruct<'a, M>
where
    M: SerializeMap + 'a,
{
    type Ok = ();
    type Error = M::Error;

    fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), Self::Error>
    where
        T: ?Sized + Serialize,
    {
        self.0.serialize_entry(key, value)
    }

    fn end(self) -> Result<(), Self::Error> {
        Ok(())
    }
}

内层是 map 时同理,FlatMapSerializeMap 逐个转发 serialize_key / serialize_value。其余的方法决定了「什么能 flatten」:

内层调了FlatMapSerializer 的处理
serialize_struct、serialize_map字段 / 条目逐个写进外层
serialize_some(v)、serialize_newtype_struct(v)剥掉一层,v.serialize(self)
serialize_none、serialize_unit、serialize_unit_struct什么也不写,Ok(())
serialize_unit_variant写一个条目 变体名: ()
serialize_newtype_variant写一个条目 变体名: 值,外部标签的 enum 正好摊成一个键
serialize_tuple_variant、serialize_struct_variant先写键 变体名;各字段先序列化进序列化一侧自己的缓冲类型(private/ser.rs 里另一个 Content),end 时作为这个键的值写出(tuple 变体写成 seq,struct 变体写成 struct)
标量、serialize_seq、tuple、tuple struct报错 can only flatten structs and maps (got ...)

内部标签的 enum 在序列化时调的是 serialize_struct(标签作为第一个字段写进去),所以也能 flatten。

为什么不能照旧 serialize_struct:它的 len 是编译期就要算出的字段数,fields: &'static str 的键名也是编译期常量,而 flatten 进来的 BTreeMap 有几个键、叫什么,要到运行时才知道。最初实现 flatten 的 #1179(「Support for Flattening」,1.0.34)在说明里写的是:「Because of current limitations on the serializer interface structs that use flattening will be serialized as maps and not as structs.」长度也给不出来,只能传 None,见 Serialize 与 Serializer 的 len 一节。src/bin/flatten_ser.rs(Kind、Block 是为最后两行加的):

#[derive(Serialize)]
struct Plain {
  title: String,
  meta: Meta,
}

#[derive(Serialize)]
struct Bad {
  title: String,
  #[serde(flatten)]
  n: u32,
}

#[derive(Serialize)]
#[serde(tag = "kind")]
enum Kind {
  Image { url: String },
}

#[derive(Serialize)]
struct Block {
  id: u32,
  #[serde(flatten)]
  kind: Kind,
  #[serde(flatten)]
  meta: Option<Meta>,
}

fn main() {
  let meta = || Meta { author: "alice".into(), views: 7 };
  let post = Post { title: "t".into(), meta: meta(), extra: BTreeMap::from([("likes".into(), 3)]) };
  println!("json     {}", serde_json::to_string(&post).unwrap());
  println!("bincode  {:?}", bincode::serialize(&post).map_err(|e| e.to_string()));
  println!("postcard {:?}", postcard::to_allocvec(&post).map_err(|e| e.to_string()));
  let plain = Plain { title: "t".into(), meta: meta() };
  println!("plain bincode {:?}", bincode::serialize(&plain).map_err(|e| e.to_string()));
  let block = Block { id: 1, kind: Kind::Image { url: "u".into() }, meta: None };
  println!("block    {}", serde_json::to_string(&block).unwrap());
  println!("bad json {:?}", serde_json::to_string(&Bad { title: "t".into(), n: 1 }).map_err(|e| e.to_string()));
}
json     {"title":"t","author":"alice","views":7,"likes":3}
bincode  Err("Bincode can only encode sequences and maps that have a knowable size ahead of time")
postcard Err("The length of a sequence must be known")
plain bincode Ok([1, 0, 0, 0, 0, 0, 0, 0, 116, 5, 0, 0, 0, 0, 0, 0, 0, 97, 108, 105, 99, 101, 7, 0, 0, 0])
block    {"id":1,"kind":"Image","url":"u"}
bad json Err("can only flatten structs and maps (got an integer)")
  • JSON 里三层的键平铺在一个对象里。
  • bincode 和 postcard 在 serialize_map(None) 这一步就报错,一个字节也没写。不带 flatten 的 Plain 走 serialize_struct,bincode 照常写出(字段按顺序直接写,没有键名)。
  • 内部标签的 Kind 摊成了 kind 和 url 两个键;meta 是 None,什么也没写。
  • u32 不能 flatten,这个错误要到运行时才出现,编译期没有检查(check.rs 只拦 flatten 用在 tuple 形式和 newtype 形式的 struct 或变体的字段上)。

反序列化:没认出的键都进 __collect

反序列化一侧,flatten 让 derive 生成的代码换了三处。先看同一个 Post 的 Deserialize(extra 换成了 HashMap<String, serde_json::Value>,views 换成 u8,cargo expand 后摘出关键几行)。

字段标识多了一个带数据的变体。 deserialize_field_identifier 在有 flatten 时用 __other(Content) 代替平常的 __ignore:

            enum __Field<'de> {
                __field0,
                __other(_serde::__private229::de::Content<'de>),
            }

__field0 只有 title:flatten 字段自己不进字段表,Meta 的 author、views 外层也不认得。__FieldVisitor 对认不出的键原样存进 __other,借得到就借:

                fn visit_borrowed_str<__E>(
                    self,
                    __value: &'de str,
                ) -> _serde::__private229::Result<Self::Value, __E>
                where
                    __E: _serde::de::Error,
                {
                    match __value {
                        "title" => _serde::__private229::Ok(__Field::__field0),
                        _ => {
                            let __value = _serde::__private229::de::Content::Str(
                                __value,
                            );
                            _serde::__private229::Ok(__Field::__other(__value))
                        }
                    }
                }

visit_u64、visit_bool 等也都写了,分别存成 Content::U64、Content::Bool:键不一定是字符串。

visit_map 里多了一个 __collect。 对应 let_collect 和 ignored_arm:

                    let mut __collect = _serde::__private229::Vec::<
                        _serde::__private229::Option<
                            (
                                _serde::__private229::de::Content,
                                _serde::__private229::de::Content,
                            ),
                        >,
                    >::new();
                    while let _serde::__private229::Some(__key) = _serde::de::MapAccess::next_key::<
                        __Field,
                    >(&mut __map)? {
                        match __key {
                            __Field::__field0 => {
                                // ... 和没有 flatten 时一样:duplicate_field 检查,next_value::<String>
                            }
                            __Field::__other(__name) => {
                                __collect
                                    .push(
                                        _serde::__private229::Some((
                                            __name,
                                            _serde::de::MapAccess::next_value_seed(
                                                &mut __map,
                                                _serde::__private229::de::ContentVisitor::new(),
                                            )?,
                                        )),
                                    );
                            }
                        }
                    }
                    // ... title 缺失时 missing_field("title")
                    let __field1: Meta = _serde::de::Deserialize::deserialize(
                        _serde::__private229::de::FlatMapDeserializer(
                            &mut __collect,
                            _serde::__private229::PhantomData,
                        ),
                    )?;
                    let __field2: HashMap<String, serde_json::Value> = _serde::de::Deserialize::deserialize(
                        _serde::__private229::de::FlatMapDeserializer(
                            &mut __collect,
                            _serde::__private229::PhantomData,
                        ),
                    )?;

没有 flatten 时,认不出的键值用 IgnoredAny 跳过,不分配;有 flatten 时,所有认不出的键值都整个读进 Content,包括本来要给 Meta 的那两个。元素类型是 Option,是为了后面「认领」时能把一项 take() 成 None 而不挪动别的项。

入口从 deserialize_struct 换成 deserialize_map。 同一个文件里,有 flatten 时 dispatch 是 deserialize_map,FIELDS 常量不生成,visit_seq 也不生成(_ if has_flatten => None):外层的字段表不完整,告诉格式也没有意义;也不再接受 JSON 数组形式的输入。

用 Content 一页加了 __deserialize_content_v1 的 LogDe 看 serde_json 收到什么,src/bin/flatten_de.rs:

#[derive(Deserialize, Debug)]
struct Meta {
  author: String,
  views: u8,
}

#[derive(Deserialize, Debug)]
struct Post {
  title: String,
  #[serde(flatten)]
  meta: Meta,
  #[serde(flatten)]
  extra: HashMap<String, serde_json::Value>,
}

fn main() {
  let input = r#"{"title":"t","author":"alice","views":7,"likes":3}"#;
  let mut de = serde_json::Deserializer::from_str(input);
  println!("=> {:?}", Post::deserialize(LogDe::new(&mut de)).unwrap());
}
deserialize_map
  visit_map
    next_key_seed
      deserialize_identifier
        visit_borrowed_str("title")
    -> Some
    next_value_seed
      deserialize_string
        visit_borrowed_str("t")
    next_key_seed
      deserialize_identifier
        visit_borrowed_str("author")
    -> Some
    next_value_seed
      __deserialize_content_v1
        visit_borrowed_str("alice")
    next_key_seed
      deserialize_identifier
        visit_borrowed_str("views")
    -> Some
    next_value_seed
      __deserialize_content_v1
        visit_u64(7)
    next_key_seed
      deserialize_identifier
        visit_borrowed_str("likes")
    -> Some
    next_value_seed
      __deserialize_content_v1
        visit_u64(3)
    next_key_seed
    -> None
=> Post { title: "t", meta: Meta { author: "alice", views: 7 }, extra: {"likes": Number(3)} }
  • 外层只发了一个 deserialize_map,没有字段表。
  • title 是认得的字段,值照常按 deserialize_string 读。
  • author、views、likes 三个值都只收到 __deserialize_content_v1:serde_json 不知道 views 将来是 u8。三个值读进 __collect,循环结束后 Meta 和 HashMap 从缓冲里取,那一段 serde_json 已经看不到了。
  • extra 里只有 likes:author、views 被 Meta 认领后删掉了(下一节)。

非自描述格式在反序列化一侧也过不去。即使拿 bincode 写出一个键正好对得上的 map(map 有长度,bincode 写得出来),读回 Post 也不行:外层走 deserialize_map,每个键要经 deserialize_identifier 认,bincode 不支持这个方法;不带 flatten 时 bincode 走的是 deserialize_struct → visit_seq,按位置读,从不需要认键名。src/bin/flatten_bincode.rs:

#[derive(Deserialize, Debug)]
struct Meta {
  author: String,
}

#[derive(Deserialize, Debug)]
struct Post {
  title: String,
  #[serde(flatten)]
  meta: Meta,
}

fn main() {
  // A map with exactly the keys Post expects, encoded by bincode (it has a length, so this works).
  let map = BTreeMap::from([("author", "alice"), ("title", "t")]);
  let bytes = bincode::serialize(&map).unwrap();
  println!("bincode  {:?}", bincode::deserialize::<Post>(&bytes).map_err(|e| e.to_string()));
  let bytes = postcard::to_allocvec(&map).unwrap();
  println!("postcard {:?}", postcard::from_bytes::<Post>(&bytes).map_err(|e| e.to_string()));
}
bincode  Err("Bincode does not support Deserializer::deserialize_identifier")
postcard Err("This is a feature that PostCard will never implement")

FlatMapDeserializer:三种取法

FlatMapDeserializer 只是一个可变借用:

pub struct FlatMapDeserializer<'a, 'de: 'a, E>(
    pub &'a mut Vec<Option<(Content<'de>, Content<'de>)>>,
    pub PhantomData<E>,
);

每个 flatten 字段各拿它一次,按字段类型发出的提示,从同一个 __collect 里取东西。取法有三种。

deserialize_struct:认领并删掉

内层是 struct 时,deserialize_struct 拿到内层的字段表 fields,交给 FlatStructAccess。它的 next_key_seed 只交出字段表里有的键,经 flat_map_take_entry:

fn flat_map_take_entry<'de>(
    entry: &mut Option<(Content<'de>, Content<'de>)>,
    recognized: &[&str],
) -> Option<(Content<'de>, Content<'de>)> {
    // Entries in the FlatMapDeserializer buffer are nulled out as they get
    // claimed for deserialization. We only use an entry if it is still present
    // and if the field is one recognized by the current data structure.
    let is_recognized = match entry {
        None => false,
        Some((k, _v)) => content_as_str(k).map_or(false, |name| recognized.contains(&name)),
    };

    if is_recognized {
        entry.take()
    } else {
        None
    }
}

认领的那一项 take() 掉,值拿走所有权,用 ContentDeserializer 重放(String 直接移交,见 Content)。不在字段表里的键,内层根本看不到。

📝NOTE
没有蠢问题

问: 我给被 flatten 的 Meta 自己加了 #[serde(deny_unknown_fields)],输入里多一个键也没报错,是不是写错了?

答: 没写错,是这个属性在这里永远不会触发。Meta 的 visit_map 只能看到 FlatStructAccess 交给它的键,而 FlatStructAccess 只交字段表里有的那些,多出来的键留在 __collect 里,Meta 不知道它们存在。要拒绝多余的键,只能在外层加,下面讲外层加了之后的行为。

deserialize_map 和 deserialize_any:只借不删

内层是 map(HashMap、BTreeMap、serde_json::Value 的对象…)时调 deserialize_map;内部标签和 untagged 的 enum 调 deserialize_any,FlatMapDeserializer 把它也转给 deserialize_map。两者都交给 FlatMapAccess,它的 next_key_seed:

        for item in &mut self.iter {
            // Items in the vector are nulled out when used by a struct.
            if let Some((ref key, ref content)) = *item {
                // Do not take(), instead borrow this entry. The internally tagged
                // enum does its own buffering so we can't tell whether this entry
                // is going to be consumed. Borrowing here leaves the entry
                // available for later flattened fields.
                self.pending_content = Some(content);
                return seed.deserialize(ContentRefDeserializer::new(key)).map(Some);
            }
        }
        Ok(None)

map 会看到所有还没被认领的项,一项也不删,用的是借用的 ContentRefDeserializer。注释给了一半理由:内部标签的 enum 自己再缓冲一次,FlatMapAccess 不知道它最后用了哪几项。另一半理由在下面这个冷知识里。

📝NOTE
冷知识:flatten 进 map 原来会把后面的字段饿死

#1299(「Allow multiple flattened maps to see the same fields」,首次发布于 1.0.65)之前,flatten 进 map 时会拿走它读到的每一项。PR 说明:「Before this change, flattening anything after a flattened map was nonsensical because the later flattened field would always observe no input fields.」例子是一个 flatten 的 map 后面跟一个 flatten 的 other,注释「always empty」。改动之后「a flattened map not consume any of the input fields, leaving them available to later flattened fields in the same struct」,理由是两个都走 deserialize_map 的 flatten 字段可能各自只关心一部分键,比如用 with 按前缀 player1_、player2_ 各取一半。也是这个 PR 加了 FlatStructAccess:struct 知道自己的字段表,可以只认领自己的键;map 不知道,就一项也不拿。

其余的提示

  • deserialize_enum:外部标签的 enum。按顺序找第一项键名是某个变体名的,take() 掉,当成 {"变体": 值} 交给 EnumDeserializer;一项都没找到报 no variant of enum X found in flattened data。和序列化一侧「摊成一个键」对应。
  • deserialize_option:见下一节。
  • deserialize_newtype_struct:剥一层,visit_newtype_struct(self)。deserialize_unit、deserialize_unit_struct、deserialize_ignored_any:直接 visit_unit,什么也不取。
  • 其余(标量、seq、tuple、identifier):can only flatten structs and maps。

Option:失败就当 None

#[serde(flatten)] meta: Option<Meta> 时,Option 的 Deserialize 调 deserialize_option。FlatMapDeserializer 没法回答「这里是不是 null」,它的做法是:

    fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
    where
        V: Visitor<'de>,
    {
        match visitor.__private_visit_untagged_option(self) {
            Ok(value) => Ok(value),
            Err(()) => Self::deserialize_other(),
        }
    }

__private_visit_untagged_option 是 Visitor 上的隐藏方法,默认 Err(()),只有 Option 的 Visitor 覆盖了它:

    fn __private_visit_untagged_option<D>(self, deserializer: D) -> Result<Self::Value, ()>
    where
        D: Deserializer<'de>,
    {
        Ok(T::deserialize(deserializer).ok())
    }

用 T 试一次,成功是 Some,任何错误都变成 None:和 untagged enum 一样靠试,Some 和 None 之间没有标记。不是 Option 的类型走默认的 Err(()),FlatMapDeserializer 再报 can only flatten structs and maps。

📝NOTE
冷知识:flatten 的 Option 到 1.0.54 才能反序列化

#1255(「Allow flattened untagged Options in struct fields」)报告:#[serde(flatten)] bar: Option<Bar> 在反序列化时报 can only flatten structs and maps,提问者只能自己写一个 #[serde(untagged)] enum OptBar { Some(Bar), None } 绕过去。#1256(「Support flattened untagged Options in struct fields」,首次发布于 1.0.54,说明只有一句「Fixes #1255.」)。改动之前,deserialize_option 在 forward_to_deserialize_other! 的列表里,直接报错;之后它单独实现,调新加的 Visitor::__private_visit_untagged_option,Option 的实现是「T 读得出来就 Some,否则 None」,行为和 issue 里那个 untagged enum 绕法相同。

这个做法的副作用是把内层的错误吞掉了,用下面这组实验的第 6 组验证。

实验:各种组合

src/bin/flatten_cases.rs,每组一个最小的结构体:

#[derive(Deserialize, Debug)]
struct Meta {
  author: String,
  views: u8,
}

// 1. deny_unknown_fields on the outer struct, flattened struct inside.
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct DenyOuter {
  title: String,
  #[serde(flatten)]
  meta: Meta,
}

// 2. deny_unknown_fields on the outer struct, flattened map inside.
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct DenyMap {
  title: String,
  #[serde(flatten)]
  extra: HashMap<String, u32>,
}

// 3. deny_unknown_fields on the flattened struct only.
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct StrictMeta {
  author: String,
}

#[derive(Deserialize, Debug)]
struct DenyInner {
  title: String,
  #[serde(flatten)]
  meta: StrictMeta,
}

// 4. Numeric map keys.
#[derive(Deserialize, Debug)]
struct NumKeys {
  title: String,
  #[serde(flatten)]
  ids: HashMap<u32, String>,
}

#[derive(Deserialize, Debug)]
struct NumKeysPlain {
  title: String,
  ids: HashMap<u32, String>,
}

// 5. Scalars inside a flattened struct.
#[derive(Deserialize, Debug)]
struct Flags {
  on: bool,
}

#[derive(Deserialize, Debug)]
struct WithFlags {
  #[serde(flatten)]
  flags: Flags,
}

#[derive(Deserialize, Debug)]
struct WithMeta {
  #[serde(flatten)]
  meta: Meta,
}

// 6. Option of a flattened struct.
#[derive(Deserialize, Debug)]
struct MaybeMeta {
  title: String,
  #[serde(flatten)]
  meta: Option<Meta>,
}

// 7. Flattened internally tagged enum, plus a flattened map after it.
#[derive(Deserialize, Debug)]
#[serde(tag = "kind")]
enum Kind {
  Image { url: String },
  Text { body: String },
}

#[derive(Deserialize, Debug)]
struct Block {
  id: u32,
  #[serde(flatten)]
  kind: Kind,
  #[serde(flatten)]
  rest: HashMap<String, serde_json::Value>,
}

#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct StrictBlock {
  id: u32,
  #[serde(flatten)]
  kind: Kind,
}

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

fn main() {
  json::<DenyOuter>("1 deny outer, known keys", r#"{"title":"t","author":"a","views":1}"#);
  json::<DenyOuter>("1 deny outer, extra key", r#"{"title":"t","author":"a","views":1,"x":0}"#);
  json::<DenyMap>("2 deny outer + map, no extra", r#"{"title":"t"}"#);
  json::<DenyMap>("2 deny outer + map, extra", r#"{"title":"t","x":0}"#);
  json::<DenyInner>("3 deny inner, extra key", r#"{"title":"t","author":"a","x":0}"#);
  json::<NumKeys>("4 flatten HashMap<u32, _>", r#"{"title":"t","1":"a"}"#);
  json::<NumKeysPlain>("4 plain HashMap<u32, _>", r#"{"title":"t","ids":{"1":"a"}}"#);
  json::<WithMeta>("5 u8 in flattened struct", r#"{"author":"a","views":7}"#);
  json::<WithMeta>("5 u8 = 300 in flattened struct", r#"{"author":"a","views":300}"#);
  json::<WithFlags>("5 bool from string", r#"{"on":"true"}"#);
  json::<MaybeMeta>("6 Option, fields present", r#"{"title":"t","author":"a","views":1}"#);
  json::<MaybeMeta>("6 Option, fields absent", r#"{"title":"t"}"#);
  json::<MaybeMeta>("6 Option, views = 300", r#"{"title":"t","author":"a","views":300}"#);
  json::<Block>("7 tagged enum + map", r#"{"id":1,"kind":"Image","url":"u","alt":"x"}"#);
  json::<Block>("7 unknown variant", r#"{"id":1,"kind":"Video","url":"u"}"#);
  json::<StrictBlock>("7 tagged enum + deny", r#"{"id":1,"kind":"Image","url":"u"}"#);
}

先猜第 6 组最后一行,再往下看。

❗IMPORTANT
动脑筋:views 是 300 时,MaybeMeta 读出什么?

Meta 的 views 是 u8,输入是 {"title":"t","author":"a","views":300}。直接读 Meta 会报 invalid value: integer `300`, expected u8。MaybeMeta 的 meta: Option<Meta> 加了 #[serde(flatten)],from_str::<MaybeMeta> 返回 Ok 还是 Err?如果是 Ok,meta 是什么?答案在文末。

其余各组的输出(第 6 组的三行放在文末):

1 deny outer, known keys           {"title":"t","author":"a","views":1}
  Ok(DenyOuter { title: "t", meta: Meta { author: "a", views: 1 } })
1 deny outer, extra key            {"title":"t","author":"a","views":1,"x":0}
  Err(unknown field `x` at line 1 column 42)
2 deny outer + map, no extra       {"title":"t"}
  Ok(DenyMap { title: "t", extra: {} })
2 deny outer + map, extra          {"title":"t","x":0}
  Err(unknown field `x` at line 1 column 19)
3 deny inner, extra key            {"title":"t","author":"a","x":0}
  Ok(DenyInner { title: "t", meta: StrictMeta { author: "a" } })
4 flatten HashMap<u32, _>          {"title":"t","1":"a"}
  Err(invalid type: string "1", expected u32 at line 1 column 21)
4 plain HashMap<u32, _>            {"title":"t","ids":{"1":"a"}}
  Ok(NumKeysPlain { title: "t", ids: {1: "a"} })
5 u8 in flattened struct           {"author":"a","views":7}
  Ok(WithMeta { meta: Meta { author: "a", views: 7 } })
5 u8 = 300 in flattened struct     {"author":"a","views":300}
  Err(invalid value: integer `300`, expected u8 at line 1 column 26)
5 bool from string                 {"on":"true"}
  Err(invalid type: string "true", expected a boolean at line 1 column 13)
7 tagged enum + map                {"id":1,"kind":"Image","url":"u","alt":"x"}
  Ok(Block { id: 1, kind: Image { url: "u" }, rest: {"url": String("u"), "alt": String("x"), "kind": String("Image")} })
7 unknown variant                  {"id":1,"kind":"Video","url":"u"}
  Err(unknown variant `Video`, expected `Image` or `Text` at line 1 column 33)
7 tagged enum + deny               {"id":1,"kind":"Image","url":"u"}
  Err(unknown field `kind` at line 1 column 33)

下面分三类讲。

flatten 与 deny_unknown_fields

两者可以一起写,编译通过,check.rs 里没有检查这个组合。外层加了 deny_unknown_fields 时,所有 flatten 字段都取完之后,生成的代码(collected_deny_unknown_fields)看 __collect 里还有没有没被删掉的项:

            if let _serde::#private::Some(_serde::#private::Some((__key, _))) =
                __collect.into_iter().filter(_serde::#private::Option::is_some).next()
            {
                if let _serde::#private::Some(__key) = _serde::#private::de::content_as_str(&__key) {
                    return _serde::#private::Err(
                        _serde::de::Error::custom(format_args!("unknown field `{}`", &__key)));
                } else {
                    return _serde::#private::Err(
                        _serde::de::Error::custom(format_args!("unexpected map key")));
                }
            }

「没被删掉」和「没人用」不是一回事,结果取决于 flatten 的是什么:

  • 第 1 组,flatten 的是 struct:能用。Meta 认领并删掉了 author、views,剩下的 x 就是真正的未知字段。
  • 第 2 组,flatten 的是 map:没有多余键时 extra 是空的;只要有一个键,extra 收下了它(借用),它却还留在 __collect 里,于是报 unknown field。结果是这个 map 永远只能是空的,有内容就失败。
  • 第 3 组,只给内层加:不生效,原因见上面的「没有蠢问题」。
  • 第 7 组最后一行,flatten 的是内部标签的 enum:输入完全合法也失败。内部标签走 deserialize_any → FlatMapAccess,只借不删,kind、url 全都留在 __collect 里,报的是第一个剩下的 kind。

所以这个组合只在「所有 flatten 字段都是 struct(或外部标签的 enum)」时按直觉工作。

类型提示丢了

第 4 组:同一个 HashMap<u32, String>,作为普通字段能读(serde_json 的 MapKey 收到 deserialize_u32 提示,把 "1" 当数字解析),flatten 之后不能:键在 __collect 里是 Content::Str("1"),重放时 deserialize_u32 对着字符串只能报错。这和内部标签里的数字键是同一个问题(#1183),机制见 Content 的「丢掉了什么」。

第 5 组:u8 字段从缓冲里读没问题,Content::U64(7) 经 deserialize_integer 变成 visit_u64(7),300 照样报范围错误;bool 字段读 "true" 失败,和直接读一样(serde_json 本来也不把字符串当布尔)。也就是说,丢掉的只是格式按提示才做的特殊处理,Visitor 自己的检查都还在。

和内部标签、untagged 不同,这里的错误带着位置。flatten 字段是在外层 visit_map 里、循环结束后取的,这时 serde_json 的 deserialize_map 还在调用栈上,错误返回时经过它的 fix_position。但位置是循环结束时读到的位置,也就是整个对象的末尾:第 1 组的 column 42 是 } 的位置,不是 "x" 所在的地方。

flatten 一个内部标签的 enum

第 7 组第一行:Kind 读对了,但 rest 里除了 alt,还有 kind 和 url。Kind 只借用了这两项,它们留在 __collect 里,后面的 map 照样看得见。想要「rest 只装剩下的」,要么不用 map,要么自己在 rest 里过滤。

内部标签自己还要再缓冲一次:TaggedContentVisitor 经 FlatMapAccess 读每一项,值的 seed 是 ContentVisitor,它调 ContentRefDeserializer 的 __deserialize_content_v1,后者把这一项 content_clone 一份。同一份数据在内存里有两份 Content。

动脑筋答案

第 6 组的三行:

6 Option, fields present           {"title":"t","author":"a","views":1}
  Ok(MaybeMeta { title: "t", meta: Some(Meta { author: "a", views: 1 }) })
6 Option, fields absent            {"title":"t"}
  Ok(MaybeMeta { title: "t", meta: None })
6 Option, views = 300              {"title":"t","author":"a","views":300}
  Ok(MaybeMeta { title: "t", meta: None })
  • 字段都在:Meta 读成功,Some。
  • 字段都不在:Meta 报 missing field,被 .ok() 变成 None。这正是 Option flatten 想要的效果。
  • views 是 300:Meta 报 invalid value: integer `300`, expected u8,同样被 .ok() 变成 None,整个 from_str 返回 Ok。author 明明是对的,也一起丢了,调用方拿不到任何报错。__private_visit_untagged_option 分不清「这组键不在」和「这组键在但值不对」,两者都是 Err,都当 None。

小结

  • 序列化:有 flatten 的结构体改调 serialize_map(None),flatten 字段经 FlatMapSerializer 把条目写进外层 map;能 flatten 的是 struct、map、Option、newtype 和各种 enum,标量和序列运行时报 can only flatten structs and maps。None 长度让 bincode、postcard 写不出来;读的一侧它们也不支持 deserialize_identifier。
  • 反序列化:入口换成 deserialize_map,__Field 多一个 __other(Content),所有认不出的键值都读成 Content 推进 Vec<Option<(Content, Content)>>,循环结束后每个 flatten 字段从这里取。
  • FlatMapDeserializer 三种取法:struct 按字段表认领并 take()(FlatStructAccess);map 和内部标签 / untagged enum 只借不删(FlatMapAccess,#1299,1.0.65);外部标签 enum 取第一个匹配变体名的项。
  • Option flatten 靠 __private_visit_untagged_option:T 读成功是 Some,任何错误都是 None(#1256,1.0.54),所以值不合法时也静默变成 None。
  • deny_unknown_fields 和 flatten 能一起编译,检查的是「__collect 里还剩没删的项」:flatten struct 时有效;flatten map 时 map 有内容就失败;flatten 内部标签 enum 时总是失败;加在内层上永远不触发。
  • 丢掉的是格式按提示做的特殊处理:flatten 进 HashMap<u32, _> 报 invalid type: string "1", expected u32;Visitor 自己的范围检查还在。错误带位置,但指向对象末尾。

💬 关联讨论

发起讨论
加载中...