Rust in Everything
基于 serde 1.0.229

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)。没有任何地方保存过各变体的错误,所以事后也取不回来。

📝NOTE
冷知识:那段 TODO 注释从 0.9 留到现在

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) 前1Float(1.0)f64 的 Visitor 接受 visit_u64
Int(i64) 在前u64::MAXFloat(1.8446744073709552e19)i64 放不下,f64 接受且不报精度损失
少字段的 struct 变体在前{"r":1.0,"label":"a"}Circle { r: 1.0 }label 被当未知字段忽略

前两种的细节在 untagged 的「第一个成功的赢」和它的动脑筋里跑过。第三种加 #[serde(untagged, deny_unknown_fields)] 就好了(ShapeFixed 两行),它作用于每个 struct 变体;也可以把字段多的变体挪到前面。

修法

按能改的东西从多到少排:

  1. 能改数据格式,就加标签。 内部标签(#[serde(tag = "type")])按标签直接选中变体,报的是那个变体自己的错误。
  2. 不能改格式,就排好顺序。 要求最多、最具体的变体放前面;数字按 i64、u64、f64 排。
  3. 只想让报错好懂,加 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;用往返测试加边界输入测试守住。

💬 关联讨论

发起讨论
加载中...