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)。不在字段表里的键,内层根本看不到。
问: 我给被 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 不知道它最后用了哪几项。另一半理由在下面这个冷知识里。
#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。
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 组最后一行,再往下看。
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。这正是Optionflatten 想要的效果。 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 取第一个匹配变体名的项。Optionflatten 靠__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自己的范围检查还在。错误带位置,但指向对象末尾。