unknown field kind
主线位置:缓冲主线。带
flatten的结构体把认不出的键值都缓冲进__collect,循环结束后交给每个 flatten 字段;外层的deny_unknown_fields最后看__collect里还剩没剩东西。机制见 flatten,这一页只讲症状和修法。
flowchart TB
coll["__collect 缓冲"] --> kind{"flatten 的是什么"}
kind -->|struct| take["认领后删掉"]
kind -->|"map 或内部标签"| keep["只借不删"]
take --> remain{"还剩键吗"}
keep --> remain
remain -->|剩| err["unknown field"]
remain -->|不剩| ok["Ok"]
本页实验在 M2 Max、macOS、rustc 1.99.0 上运行,依赖 serde = "=1.0.229"(derive)、serde_json = "=1.0.154"。
现象
一个块有 id,其余字段由内部标签的 Kind 决定。外层加了 deny_unknown_fields,想拒绝拼错的键。输入完全合法,却报错。src/bin/flatten_deny.rs:
#[derive(Deserialize, Debug)]
#[serde(tag = "kind")]
enum Kind {
Image { url: String },
Text { body: String },
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct Block {
id: u32,
#[serde(flatten)]
kind: Kind,
}
tagged enum {"id":1,"kind":"Image","url":"u"}
Err(unknown field `kind` at line 1 column 33)
kind 是标签,Kind 明明用了它。把 kind 从输入里删掉(src/bin/flatten_more.rs,同样的 Block):
no tag {"id":1,"url":"u"}
Err(missing field `kind` at line 1 column 18)
有标签报多余,没标签报缺失:这个结构体没有能通过的输入。flatten_deny.rs 里另外几组(Meta 只有一个 author: String 字段):
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct Plain {
title: String,
author: String,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct WithStruct {
title: String,
#[serde(flatten)]
meta: Meta,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct WithMap {
title: String,
#[serde(flatten)]
extra: HashMap<String, u32>,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct StrictMeta {
author: String,
}
#[derive(Deserialize, Debug)]
struct InnerDeny {
title: String,
#[serde(flatten)]
meta: StrictMeta,
}
plain {"title":"t","author":"a","x":0}
Err(unknown field `x`, expected `title` or `author` at line 1 column 29)
struct {"title":"t","author":"a"}
Ok(WithStruct { title: "t", meta: Meta { author: "a" } })
struct {"title":"t","author":"a","x":0}
Err(unknown field `x` at line 1 column 32)
map {"title":"t"}
Ok(WithMap { title: "t", extra: {} })
map {"title":"t","n":1}
Err(unknown field `n` at line 1 column 19)
inner deny {"title":"t","author":"a","x":0}
Ok(InnerDeny { title: "t", meta: StrictMeta { author: "a" } })
四种行为:
| flatten 的是 | deny_unknown_fields 写在 | 结果 |
|---|---|---|
| struct | 外层 | 按直觉工作 |
| map | 外层 | map 里只要有一项就报错,只能是空的 |
| 内部标签 enum | 外层 | 合法输入也报错,报的是标签名 |
| struct | 内层 | 永远不触发 |
报错的文字也和普通的不一样:plain 那行是 unknown field `x`, expected `title` or `author` ,位置在 "x" 后面;有 flatten 时只有 unknown field `x` ,没有候选列表,位置是对象末尾。看到不带 expected 的 unknown field,基本可以确定是 flatten 这一条路报的。
出处
普通结构体在读到认不出的键时,当场调 Error::unknown_field,带上字段表。有 flatten 时认不出的键不能当场拒绝,它可能属于某个 flatten 字段,所以先进 __collect;所有 flatten 字段取完之后,collected_deny_unknown_fields 找第一项还没被删掉的:
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)));
这里用的是 custom,没有字段表可给,所以没有 expected。检查的是「还在不在」,而 FlatMapDeserializer 对不同的字段类型取法不同:struct 经 FlatStructAccess 认领并 take();map 经 FlatMapAccess 只借用、不删;内部标签 enum 调 deserialize_any,被转给 deserialize_map,同样只借不删。kind、url 都还在 __collect 里,报的是第一项 kind。内层的 deny_unknown_fields 不触发,是因为 FlatStructAccess 只把内层字段表里有的键交给它。为什么 map 只借不删,见 flatten 的「只借不删」和那里关于 #1299 的冷知识。
问: 这个组合有一种情况永远失败,为什么 serde_derive 不在编译期报错?
答: 因为宏看不到。derive 处理 Block 时,kind: Kind 只是一串记号,Kind 是 struct、map 还是内部标签 enum,要等类型检查之后才知道,而那时宏早就展开完了。check.rs 里和 flatten 有关的检查(check_flatten_field)只拦得住看语法就能判断的情况:tuple struct、newtype struct 上的 flatten。它也不能一刀切地禁止这个组合,因为 flatten struct 的情况能正常工作;issue #2384 就是请求把「简单情况能用」写进文档。内部标签这一种在 issue #1358(2018-08,serde 1.0.70)里就被报告过,原话是「This panics with „unknown field type“」,「it can't make its mind if it wants type or not」,到 1.0.229 仍是 open。
另一个静默的:flatten 的 Option
deny_unknown_fields 是「不该报的报了」,#[serde(flatten)] meta: Option<Meta> 是反过来:该报的不报。FlatMapDeserializer 回答不了「这里是不是 null」,于是用 Meta 试一次,任何错误都当成 None(__private_visit_untagged_option,见 flatten 的「Option:失败就当 None」)。src/bin/flatten_fix.rs 的一部分:
#[derive(Deserialize, Debug)]
struct Meta {
author: String,
views: u8,
}
#[derive(Deserialize, Debug)]
struct MaybeMeta {
title: String,
#[serde(flatten)]
meta: Option<Meta>,
}
MaybeMeta {"title":"t"}
Ok(MaybeMeta { title: "t", meta: None })
MaybeMeta {"title":"t","author":"a","views":300}
Ok(MaybeMeta { title: "t", meta: None })
第二行的 views 超出 u8,from_str 却返回 Ok,author 也一起丢了。
修法
- flatten struct:照常用外层的
deny_unknown_fields。 - flatten map:两者本来矛盾,map 就是用来收「其余的键」的。去掉
deny_unknown_fields,需要的话自己检查 map 的键。 - flatten 内部标签 enum:把
deny_unknown_fields从外层挪到 enum 上。它作用于每个 struct 变体,变体看得到__collect里所有还没被删掉的键,多出来的就报错。旁边还有别的 flatten 字段时要注意声明顺序,见下面的实验。 - 内层的
deny_unknown_fields:删掉,它不做任何事。 - flatten 的
Option<T>:改成 flatten 一个非Option的 struct,把可以缺的字段各自写成Option。缺失照样是None,值不对就报错。
flatten_fix.rs 的其余部分:
// Fix 1: deny on the flattened enum, not on the outer struct.
#[derive(Deserialize, Debug)]
#[serde(tag = "kind", deny_unknown_fields)]
enum Kind {
Image { url: String },
Text { body: String },
}
#[derive(Deserialize, Debug)]
struct Block {
id: u32,
#[serde(flatten)]
kind: Kind,
}
// Fix 2: flatten a struct whose fields are Option.
#[derive(Deserialize, Debug)]
struct LooseMeta {
author: Option<String>,
views: Option<u8>,
}
#[derive(Deserialize, Debug)]
struct Post {
title: String,
#[serde(flatten)]
meta: LooseMeta,
}
Block {"id":1,"kind":"Image","url":"u"}
Ok(Block { id: 1, kind: Image { url: "u" } })
Block {"id":1,"kind":"Image","url":"u","x":0}
Err(unknown field `x`, expected `url` at line 1 column 39)
Post {"title":"t"}
Ok(Post { title: "t", meta: LooseMeta { author: None, views: None } })
Post {"title":"t","author":"a","views":300}
Err(invalid value: integer `300`, expected u8 at line 1 column 38)
- 合法的
Block能读了,多一个x由变体Image报出,带expected,候选只有变体自己的字段url。 Post缺字段是None,views是 300 时报错,MaybeMeta吞掉的错误回来了。
第一种修法和别的 flatten 字段放在一起时,结果取决于声明顺序:flatten 字段按声明顺序从 __collect 里取,struct 取走的键后面的人就看不到了。flatten_more.rs,StrictKind 就是上面加了 deny_unknown_fields 的 Kind(只留 Image 一个变体),Meta 只有 author:
#[derive(Deserialize, Debug)]
struct EnumThenStruct {
#[serde(flatten)]
kind: StrictKind,
#[serde(flatten)]
meta: Meta,
}
#[derive(Deserialize, Debug)]
struct StructThenEnum {
#[serde(flatten)]
meta: Meta,
#[serde(flatten)]
kind: StrictKind,
}
enum, struct {"kind":"Image","url":"u","author":"a"}
Err(unknown field `author`, expected `url` at line 1 column 39)
struct, enum {"kind":"Image","url":"u","author":"a"}
Ok(StructThenEnum { meta: Meta { author: "a" }, kind: Image { url: "u" } })
enum 在前时,author 还在 __collect 里,被变体当成多余的键;struct 在前时,Meta 先把 author 认领走了。所以加了 deny_unknown_fields 的 enum 要放在所有 flatten struct 之后。flatten 的 map 放在哪里都没用:它只借不删,map 想收的多余键 enum 照样看得到。src/bin/flatten_map_enum.rs 把 rest: HashMap<String, serde_json::Value> 放在 StrictKind 前面:
Ok(MapThenEnum { rest: {"kind": String("Image"), "url": String("u")}, kind: Image { url: "u" } })
Err(Error("unknown field `x`, expected `url`", line: 1, column: 32))
x 进了 rest,也被 Image 拒绝;rest 能装的只有 enum 自己用掉的那几个键。
怎么确认没中招
- 搜同时出现
deny_unknown_fields和flatten的结构体,按上面的表看 flatten 的每个字段是什么类型。 - 对每个带 flatten 的类型写一个往返测试:
to_string一个合法值再from_str。外层 deny 加内部标签 enum 的组合连自己写出的 JSON 都读不回来,这个测试第一次跑就会红(flatten_more.rs,Block加上Serialize):
roundtrip {"id":1,"kind":"Image","url":"u"}
Err(unknown field `kind` at line 1 column 33)
- 对每个
flatten的Option字段,喂一份「键都在、但有一个值不合法」的输入,确认它报错而不是得到None。
小结
- flatten 加
deny_unknown_fields能编译,检查的是「__collect里还剩没删的项」:struct 正常;map 只能为空;内部标签 enum 合法输入也报unknown field `kind`;写在内层永远不触发。 - 这条路报的
unknown field用custom造,没有expected列表,位置在对象末尾;看到这种格式就先查 flatten。 - 宏在展开时不知道字段类型是 struct 还是 enum,所以拦不住;#1358 从 1.0.70 起开着。
- flatten 内部标签 enum 时,把
deny_unknown_fields挪到 enum 上,并把它声明在所有 flatten struct 之后。 - flatten 的
Option<T>把任何错误变成None;改成 flatten 一个字段各自为Option的 struct。