data did not match any variant of untagged enum Shape
主线位置:缓冲主线。untagged enum 先把输入读成
Content,再按声明顺序逐个变体重放,第一个Ok赢,全失败时只报一句固定的话。机制见 untagged,这一页只讲碰到之后怎么办。
flowchart TB
err["no match 一句话"] --> v["读成 Value"]
v --> loc["找到失败的那个元素"]
loc --> each["逐个变体 from_value"]
each --> why["各自的真实错误"]
ok["读成功了"] --> rt{"往返相等吗"}
rt -->|否| order["变体顺序吞掉了输入"]
rt -->|是| fine["没中招"]
本页实验在 M2 Max、macOS、rustc 1.99.0 上运行,依赖 serde = "=1.0.229"(derive)、serde_json = "=1.0.154"。
现象
一个文档里有一组形状,每个形状是圆或矩形,用 untagged 表示。第二个形状的 h 写成了字符串。src/bin/untagged_symptom.rs 的前半段:
#[derive(Deserialize, Debug)]
struct Circle {
r: f64,
}
#[derive(Deserialize, Debug)]
struct Rect {
w: f64,
h: f64,
}
#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum Shape {
Circle(Circle),
Rect(Rect),
}
#[derive(Deserialize, Debug)]
struct Doc {
name: String,
shapes: Vec<Shape>,
}
const INPUT: &str = r#"{
"name": "demo",
"shapes": [
{"r": 1.0},
{"w": 1.0, "h": "2"},
{"r": 3.0}
]
}"#;
fn main() {
match serde_json::from_str::<Doc>(INPUT) {
Ok(d) => println!("{d:?}"),
Err(e) => println!("Display: {e}\nDebug: {e:?}"),
}
// ...
}
Display: data did not match any variant of untagged enum Shape at line 6 column 4
Debug: Error("data did not match any variant of untagged enum Shape", line: 6, column: 4)
错误里有三样东西不对劲:
- 没说哪个变体、为什么。
Circle失败是因为缺r,Rect失败是因为h类型不对,两条信息都没了。 - 位置不是出错的地方。 第 6 行第 4 列是第三个形状
{"r": 3.0}的{之前,出错的是第 5 行。serde_json 的deserialize_seq在visit_seq出错后仍调end_seq,它吃掉逗号和空白,然后fix_position把这时读到的位置补给这个本来没有位置的错误。 - 顶层就没有位置。 直接
from_str::<Shape>时是line: 0, column: 0,原因见 untagged 的「报错只剩一句」。
出处
这句话是 serde_derive 在编译期拼好、写进生成代码的字符串字面量,enum_untagged.rs 的 deserialize:
let fallthrough_msg = format!(
"data did not match any variant of untagged enum {}",
params.type_name()
);
let fallthrough_msg = cattrs.expecting().unwrap_or(&fallthrough_msg);
生成的代码对每个变体写一个 if let Ok(__ok) = .. { return .. },Err 没有绑定,直接丢弃;全部落空后调 de::Error::custom(fallthrough_msg)。没有任何地方保存过各变体的错误,所以事后也取不回来。
fallthrough_msg 上面那段注释(L32)开头是「TODO this message could be better by saving the errors from the failed attempts.」。issue #773(2017-02,「Better message when failing to match any variant of an untagged enum」)引用的就是 v0.9.7 里同样一段,正文只有一句「This message could be a lot better.」,2019-02 被关闭。之后想把各变体的错误收集起来的 PR 有好几个:#1544(「Collect errors when deserializing untagged enums」,用一个 verbose-debug feature 打开)、#2814、#3003,都没有合并就关闭了;#3047、#3094 到 1.0.229 仍开着。真正发布的是另一条路:#1916 加了容器属性 expecting,首次发布于 1.0.122,发布说明写「Add attribute to override default deserialization failure expectation message」,PR 说明里对 untagged 的说法是「attribute completely replaces error message」。也就是上面第二行的 cattrs.expecting():能换掉这句话,但换不出变体的错误。
调试:逐个变体重试
serde 不给,就自己在外面重做一遍「逐个尝试」,这次留下错误。先把输入读成 serde_json::Value(不会因为 h 的类型失败),找到读不成 Shape 的那个元素,再对每个变体的内容类型单独 from_value。untagged_symptom.rs 的后半段:
fn try_as<T: DeserializeOwned>(name: &str, v: &Value) {
match serde_json::from_value::<T>(v.clone()) {
Ok(_) => println!(" {name}: ok"),
Err(e) => println!(" {name}: {e}"),
}
}
fn main() {
// ...
println!("-- per variant --");
let doc: Value = serde_json::from_str(INPUT).unwrap();
for (i, shape) in doc["shapes"].as_array().unwrap().iter().enumerate() {
if serde_json::from_value::<Shape>(shape.clone()).is_err() {
println!(" shapes[{i}] = {shape}");
try_as::<Circle>("Circle", shape);
try_as::<Rect>("Rect", shape);
}
}
}
-- per variant --
shapes[1] = {"h":"2","w":1.0}
Circle: missing field `r`
Rect: invalid type: string "2", expected f64
- 第一步定位:
shapes[1],不是错误信息里的第 6 行指向的shapes[2]。 - 第二步看各变体:
Rect那一行就是原因。哪个变体是「本来想要的」,要自己判断:这里Circle缺的是它唯一的字段,Rect的两个键都在、只是一个值的类型不对,后者显然更接近。 - 这个办法要求变体是 newtype,内容是一个有名字的类型;struct 变体要先拆成 newtype 加独立的 struct 才能单独试。
from_value走的是Value的Deserializer,不是 serde 内部的ContentRefDeserializer,两者对绝大多数输入行为相同,但不是同一份代码。
只在调试时这样做。上线的代码里每次失败都多解析两遍,而且 Value 这一层的错误同样没有输入里的位置。
另一面:读成功了,但读错了
untagged 按声明顺序试,第一个 Ok 就返回,后面的变体看都不看。于是有一类问题不报错:输入被前面的变体抢走。src/bin/untagged_order.rs,对每个值做一次 to_string 再 from_str,看回来的是不是原值:
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(untagged)]
enum FloatFirst {
Float(f64),
Int(i64),
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(untagged)]
enum IntFirst {
Int(i64),
Float(f64),
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(untagged)]
enum Shape {
Circle { r: f64 },
Labeled { r: f64, label: String },
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(untagged, deny_unknown_fields)]
enum ShapeFixed {
Circle { r: f64 },
Labeled { r: f64, label: String },
}
fn roundtrip<T>(v: T)
where
T: Serialize + for<'de> Deserialize<'de> + std::fmt::Debug + PartialEq,
{
let s = serde_json::to_string(&v).unwrap();
let back: T = serde_json::from_str(&s).unwrap();
let mark = if back == v { "ok " } else { "LOST" };
println!("{mark} {v:?} -> {s} -> {back:?}");
}
fn main() {
roundtrip(FloatFirst::Int(1));
roundtrip(IntFirst::Int(1));
println!("{:?}", serde_json::from_str::<IntFirst>("18446744073709551615"));
roundtrip(Shape::Labeled { r: 1.0, label: "a".into() });
roundtrip(ShapeFixed::Labeled { r: 1.0, label: "a".into() });
roundtrip(ShapeFixed::Circle { r: 1.0 });
}
LOST Int(1) -> 1 -> Float(1.0)
ok Int(1) -> 1 -> Int(1)
Ok(Float(1.8446744073709552e19))
LOST Labeled { r: 1.0, label: "a" } -> {"r":1.0,"label":"a"} -> Circle { r: 1.0 }
ok Labeled { r: 1.0, label: "a" } -> {"r":1.0,"label":"a"} -> Labeled { r: 1.0, label: "a" }
ok Circle { r: 1.0 } -> {"r":1.0} -> Circle { r: 1.0 }
三种吞法:
| 写法 | 输入 | 结果 | 原因 |
|---|---|---|---|
Float(f64) 在 Int(i64) 前 | 1 | Float(1.0) | f64 的 Visitor 接受 visit_u64 |
Int(i64) 在前 | u64::MAX | Float(1.8446744073709552e19) | i64 放不下,f64 接受且不报精度损失 |
| 少字段的 struct 变体在前 | {"r":1.0,"label":"a"} | Circle { r: 1.0 } | label 被当未知字段忽略 |
前两种的细节在 untagged 的「第一个成功的赢」和它的动脑筋里跑过。第三种加 #[serde(untagged, deny_unknown_fields)] 就好了(ShapeFixed 两行),它作用于每个 struct 变体;也可以把字段多的变体挪到前面。
修法
按能改的东西从多到少排:
- 能改数据格式,就加标签。 内部标签(
#[serde(tag = "type")])按标签直接选中变体,报的是那个变体自己的错误。 - 不能改格式,就排好顺序。 要求最多、最具体的变体放前面;数字按
i64、u64、f64排。 - 只想让报错好懂,加
expecting。 它替换整句话,不增加信息。
src/bin/untagged_fix.rs:
#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum Num {
Int(i64),
UInt(u64),
Float(f64),
}
#[derive(Deserialize, Debug)]
#[serde(untagged, expecting = "a circle {r} or a rect {w, h}")]
enum Shape {
Circle { r: f64 },
Rect { w: f64, h: f64 },
}
#[derive(Deserialize, Debug)]
#[serde(tag = "type")]
enum TaggedShape {
Circle { r: f64 },
Rect { w: f64, h: f64 },
}
fn main() {
for s in ["-1", "1", "18446744073709551615", "1.5"] {
println!("{s:<22} => {:?}", serde_json::from_str::<Num>(s));
}
println!("{:?}", serde_json::from_str::<Shape>(r#"{"w":1.0,"h":"2"}"#));
println!("{:?}", serde_json::from_str::<TaggedShape>(r#"{"type":"Rect","w":1.0,"h":"2"}"#));
}
-1 => Ok(Int(-1))
1 => Ok(Int(1))
18446744073709551615 => Ok(UInt(18446744073709551615))
1.5 => Ok(Float(1.5))
Err(Error("a circle {r} or a rect {w, h}", line: 0, column: 0))
Err(Error("invalid type: string \"2\", expected f64", line: 0, column: 0))
Num三个变体排好之后,四种数字各归各位。expecting只换了措辞,位置照样是 0。- 内部标签直接说出
invalid type: string "2", expected f64。它也缓冲、也是重放之后才出错,所以同样没有位置(见 Content),但至少原因是对的。
怎么确认没中招
- 每个变体写一个往返测试:构造值、
to_string、from_str、assert_eq!。上面LOST的两行就是这样抓出来的。它只覆盖你构造得出的值:u64::MAX不是IntFirst的任何一个变体能装下的整数,往返测试抓不到,要用输入一侧的测试(把边界值字符串直接from_str)补。 - 给每个变体留一条该失败的输入,确认报的是
data did not match any variant,而不是被别的变体悄悄接住。 - 搜一下
#[serde(untagged)]里有没有f64排在整数前面、字段少的 struct 变体排在字段多的前面。
小结
- 这句话是编译期写死的字符串,各变体的错误生成代码里根本没有保存;
expecting(1.0.122,#1916)只能换掉措辞。 - 嵌在序列里时,位置指向失败元素后面;顶层没有位置。
- 调试:读成
serde_json::Value,先找出失败的元素,再对每个变体的内容类型单独from_value。 - 更隐蔽的是不报错的情况:
f64吃掉整数,u64::MAX落进f64,少字段的 struct 变体吃掉多字段的输入。 - 修法:能加标签就加标签;否则排好变体顺序、给 struct 变体加
deny_unknown_fields;用往返测试加边界输入测试守住。