Rust in Everything
基于 serde 1.0.229

untagged

主线位置:缓冲主线最典型的一支。生成的代码先用 ContentVisitor 把输入整段读成 Content(第一遍,格式驱动),再拿同一份 Content 按变体声明顺序一个个重放反序列化主线(第二遍起,ContentRefDeserializer 驱动),第一个返回 Ok 的变体赢;全部失败时用 de::Error::custom 报一句固定的话。序列化一侧没有缓冲:变体直接按内容序列化,不写任何标签。

flowchart TB
  gen["生成的 deserialize"] --> cv["ContentVisitor 读成 Content"]
  cv --> ref["ContentRefDeserializer"]
  ref --> v1["试第 1 个变体"]
  v1 -->|Ok| win[返回]
  v1 -->|Err 丢弃| v2["试第 2 个变体"]
  v2 -->|Ok| win
  v2 -->|Err 丢弃| more["……"]
  more --> fail["custom 一句话"]

Content 的定义、它没有的 u128 / i128、ContentRefDeserializer 怎么把 deserialize_u8 之类的提示映射到 Content 的几种整数,都在 Content:缓冲的代价。内部标签也缓冲,但只重放一次,见内部标签与相邻标签。本页实验都在 M2 Max、macOS、rustc 1.99.0 上运行,依赖 serde 1.0.229、serde_json 1.0.154。

生成的代码

enum_untagged.rs 的 deserialize 的模板:

    quote_block! {
        let __content = _serde::de::DeserializeSeed::deserialize(_serde::#private::de::ContentVisitor::new(), __deserializer)?;
        let __deserializer = _serde::#private::de::ContentRefDeserializer::<__D::Error>::new(&__content);

        #first_attempt

        #(
            if let _serde::#private2::Ok(__ok) = #attempts {
                return _serde::#private2::Ok(__ok);
            }
        )*

        _serde::#private::Err(_serde::de::Error::custom(#fallthrough_msg))
    }

对一个两变体的 enum 展开:

#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum Shape {
  Circle(Circle),   // struct Circle { r: f64 }
  Rect(Rect),       // struct Rect { w: f64, h: f64 }
}
let __content = _serde::de::DeserializeSeed::deserialize(
    _serde::__private229::de::ContentVisitor::new(),
    __deserializer,
)?;
let __deserializer = _serde::__private229::de::ContentRefDeserializer::<
    __D::Error,
>::new(&__content);
if let _serde::__private229::Ok(__ok) = _serde::__private229::Result::map(
    <Circle as _serde::Deserialize>::deserialize(__deserializer),
    Shape::Circle,
) {
    return _serde::__private229::Ok(__ok);
}
if let _serde::__private229::Ok(__ok) = _serde::__private229::Result::map(
    <Rect as _serde::Deserialize>::deserialize(__deserializer),
    Shape::Rect,
) {
    return _serde::__private229::Ok(__ok);
}
_serde::__private229::Err(
    _serde::de::Error::custom(
        "data did not match any variant of untagged enum Shape",
    ),
)
  • 第一遍只有一个 ?。 ContentVisitor 作为 seed 调 __deserialize_content_v1,默认实现就是 deserialize_any。这里的错误是格式自己的错误(语法错、EOF),原样带位置返回。
  • 之后每个变体一个 if let Ok。 Err 分支不存在:失败的结果被 if let 直接丢掉,连错误值都没有绑定。
  • 同一个 __deserializer 用了多次。 Deserializer 的方法按值拿 self,能这样写是因为 ContentRefDeserializer 只是 &Content 加一个 PhantomData,实现了 Copy。每次尝试拿到的都是一份新的拷贝,从树根开始读。也因为要读很多次,这里只能借用 Content,不能用拿走所有权的 ContentDeserializer。借用的代价是:Content::String 这样拥有所有权的节点,ContentRefDeserializer::deserialize_str 只能交出 visit_str(v),读成 String 时要再复制一份。

#attempts 由 deserialize_variant 逐个变体生成,按形状分四种:

变体生成的尝试面对 ContentRefDeserializer 调的方法
unitdeserialize_any(UntaggedUnitVisitor)deserialize_any
newtype V(T)T::deserialize(__deserializer)T 自己决定
tuple V(A, B)TupleForm::Untagged 的代码deserialize_tuple(2, ..)
struct V { .. }StructForm::Untagged 的代码deserialize_any,只有 visit_map

unit 变体用的 UntaggedUnitVisitor 只实现了 visit_unit 和 visit_none,所以 untagged 的 unit 变体对应的是 JSON 的 null,不是变体名。序列化一侧 serialize_untagged_variant 对 unit 变体也只调 serialize_unit:

#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
enum Limit {
  Unlimited,
  Max(u32),
}
null
null         => Ok(Unlimited)
5            => Ok(Max(5))
"Unlimited"  => Err(Error("data did not match any variant of untagged enum Limit", line: 0, column: 0))

第一行是 to_string(&Limit::Unlimited),后三行是 from_str。

struct 变体那一行的「只有 visit_map」来自 struct_.rs 里的一句注释:「untagged struct variants do not get a visit_seq method.」普通 struct 能从 JSON 数组读出来(见生成的 Deserialize),untagged 的 struct 变体不行,newtype 变体里包一个 struct 却可以,因为那时用的是那个 struct 自己的 Deserialize:

struct   Ok(Point { x: 1 })
variant  Err(Error("data did not match any variant of untagged enum A", line: 0, column: 0))
newtype  Ok(P(Point { x: 1 }))

三行的输入都是 [1],类型依次是 struct Point { x: i32 }、#[serde(untagged)] enum A { P { x: i32 } }、#[serde(untagged)] enum B { P(Point) }(src/bin/untagged_seq.rs)。

实验:每次尝试从哪里读

和内部标签那一页一样,用 Logged<T> 包住变体内容:它的 Deserialize 先打印拿到的 Deserializer 类型名,再用打印调用的 LogDe 包住往下读。src/bin/untagged_log.rs:

#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum Value {
  Flag(Logged<bool>),
  Circle(Logged<Circle>),
}

fn main() {
  println!("{:?}", serde_json::from_str::<Value>(r#"{"r":2.5}"#));
}
[bool gets serde::private::de::content::ContentRefDeserializer]
  deserialize_bool
[Circle gets serde::private::de::content::ContentRefDeserializer]
  deserialize_struct("Circle", ["r"])
    visit_map
      next_key_seed
        deserialize_identifier
          visit_borrowed_str("r")
      next_value_seed
        deserialize_f64
          visit_f64(2.5)
      next_key_seed
        => None
Ok(Circle(Logged(Circle { r: 2.5 })))
  • 两次尝试拿到的都是 ContentRefDeserializer,不是 serde_json。到这里 serde_json 已经把 {"r":2.5} 读完了。
  • bool 的尝试调 deserialize_bool,下面没有任何 visit_*:Content::Map 不是布尔值,ContentRefDeserializer 直接报 invalid type,这个错误被 if let 扔掉。
  • Circle 的尝试从树根重新开始,"r" 仍是借用的字符串。

第一个成功的赢

尝试的顺序就是变体的声明顺序,一旦某个变体返回 Ok 就 return,后面的变体看都不看。于是「能读」不等于「读对」。src/bin/untagged_order.rs:

#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum Num {
  Float(f64),
  Int(i64),
}

#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum Shape {
  Circle { r: f64 },
  Labeled { r: f64, label: String },
}

#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum Shape2 {
  Labeled { r: f64, label: String },
  Circle { r: f64 },
}

#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum Id {
  Text(String),
  Num(u64),
}

fn main() {
  println!("{:?}", serde_json::from_str::<Num>("1"));
  println!("{:?}", serde_json::from_str::<Shape>(r#"{"r":1.0,"label":"a"}"#));
  println!("{:?}", serde_json::from_str::<Shape2>(r#"{"r":1.0,"label":"a"}"#));
  println!("{:?}", serde_json::from_str::<Id>("42"));
  println!("{:?}", serde_json::from_str::<Id>(r#""42""#));
}
Ok(Float(1.0))
Ok(Circle { r: 1.0 })
Ok(Labeled { r: 1.0, label: "a" })
Ok(Num(42))
Ok(Text("42"))
  • 整数被 f64 抢走。 1 在 Content 里是 U64(1),f64 的 Visitor 实现了 visit_u64(转成浮点数),所以 Float 在前就是 Float(1.0),Int 永远轮不到整数。
  • 少字段的 struct 变体抢走多字段的输入。 Circle { r } 没有 deny_unknown_fields,"label" 被当作未知字段忽略,Circle 成功。把 Labeled 放到前面(Shape2)才得到想要的结果。另一个办法是在 enum 上加 #[serde(untagged, deny_unknown_fields)],它作用于每个 struct 变体,同样的输入会得到 Labeled(src/bin/untagged_deny.rs 里跑过)。经验是:把要求最多、最具体的变体放在前面。
  • 字符串和数字不会互相转换。 42 不是字符串,String 的尝试失败,落到 Num;"42" 是字符串,Text 成功。缓冲本身不会改变「是什么类型」,Content 保留了 serde_json 在第一遍报出的类型。
❗IMPORTANT
动脑筋:u64 的最大值落到哪个变体?

平台 M2 Max、macOS、rustc 1.99.0。把上面的 Num 反过来,整数在前(src/bin/untagged_quiz.rs):

#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum Num {
  Int(i64),
  Float(f64),
}

fn main() {
  for s in ["1", "-1", "1.0", "18446744073709551615"] {
    println!("{s:<22} => {:?}", serde_json::from_str::<Num>(s));
  }
}

最后一个输入是 u64::MAX。四行各打印什么?

报错只剩一句

全部变体都失败时的消息在 deserialize 里拼好,作为字符串字面量写进生成的代码:

    // TODO this message could be better by saving the errors from the failed
    // attempts. The heuristic used by TOML was to count the number of fields
    // processed before an error, and use the error that happened after the
    // largest number of fields. I'm not sure I like that. Maybe it would be
    // better to save all the errors and combine them into one message that
    // explains why none of the variants matched.
    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);

这段 TODO 注释 说得很清楚:每次尝试的错误都没保存,所以没法说哪个变体为什么失败。可以换掉的只有这句话本身:容器属性 #[serde(expecting = "...")] 给了值就用它(见属性)。最后由 de::Error::custom 造出错误,用的是格式的错误类型,对 serde_json 就是 serde_json::Error。

src/bin/untagged_err.rs,Shape 同上,Shape2 多一个 expecting,Doc 是一个外层 struct:

#[derive(Deserialize, Debug)]
#[serde(untagged, expecting = "a circle {r} or a rect {w, h}")]
enum Shape2 {
  Circle(Circle),
  Rect(Rect),
}

#[derive(Deserialize, Debug)]
struct Doc {
  name: String,
  shape: Shape,
}

fn main() {
  let bad = r#"{"w":1.0,"h":"2"}"#;
  println!("{:?}", serde_json::from_str::<Shape>(bad));
  println!("{:?}", serde_json::from_str::<Shape2>(bad));
  let doc = r#"{"name":"a",
"shape":{"w":1.0,"h":"2"}}"#;
  println!("{:?}", serde_json::from_str::<Doc>(doc));
  println!("{:?}", serde_json::from_str::<Shape>(r#"{"w":1.0,"h":"#));
}
Err(Error("data did not match any variant of untagged enum Shape", line: 0, column: 0))
Err(Error("a circle {r} or a rect {w, h}", line: 0, column: 0))
Err(Error("data did not match any variant of untagged enum Shape", line: 2, column: 26))
Err(Error("EOF while parsing a value", line: 1, column: 13))
  • 顶层的 untagged 错误没有位置。 line: 0 表示没有位置。custom 造错误时不知道位置,serde_json 的 Deserializer 在 Visitor 返回后才用 fix_position 补(见错误)。这个错误是在 serde_json 的 deserialize_any 返回之后才造出来的,没有人再补。
  • 嵌在别的类型里就有位置,但不是出错的位置。 Doc 一行的位置是 line: 2, column: 26,是整个外层对象读完以后的位置。错误从 Doc 的 visit_map 冒出来,经过 serde_json 外层的 deserialize_struct,它先 end_map(),再对错误调 fix_position。真正有问题的 "2" 在第 2 行第 22 列,这个信息已经没了。
  • 语法错误不受影响。 最后一行是第一遍里 serde_json 自己报的 EOF,位置正常。

怎么查是哪个变体失败

serde 没有提供办法,只能在外面重做一遍「逐个尝试」,这次把错误留下来。把输入先读成 serde_json::Value(它和 Content 一样,是 deserialize_any 读出来的一棵树),再对每个变体的内容类型单独 from_value。src/bin/untagged_debug.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() {
  let s = r#"{"w":1.0,"h":"2"}"#;
  if let Err(e) = serde_json::from_str::<Shape>(s) {
    println!("Shape: {e}");
    let v: Value = serde_json::from_str(s).unwrap();
    try_as::<Circle>("Circle", &v);
    try_as::<Rect>("Rect", &v);
  }
}
Shape: data did not match any variant of untagged enum Shape
  Circle: missing field `r`
  Rect: invalid type: string "2", expected f64

Rect 那一行就是答案:h 写成了字符串。这个办法要求每个变体都是 newtype(内容是一个有名字的类型),struct 变体得先改写成 newtype 加一个独立的 struct 才能单独试。它重放的是 serde_json::Value 的 Deserializer,不是 ContentRefDeserializer,两者是不同的代码,调试时要记住这一层差别。

单个变体上的 #[serde(untagged)]

#[serde(untagged)] 也可以写在变体上:前面的变体照常带标签,后面标了 untagged 的变体作为兜底。enum_.rs 的 deserialize 在这里分岔:

    // The variants have already been checked (in ast.rs) that all untagged variants appear at the end
    match variants.iter().position(|var| var.attrs.untagged()) {
        Some(variant_idx) => {
            let (tagged, untagged) = variants.split_at(variant_idx);
            let tagged_frag = Expr(deserialize_homogeneous_enum(params, tagged, cattrs));
            // Ignore any error associated with non-untagged deserialization so that we
            // can fall through to the untagged variants. This may be infallible so we
            // need to provide the error type.
            let first_attempt = quote! {
                if let _serde::#private::Result::<_, __D::Error>::Ok(__ok) = (|| #tagged_frag)() {
                    return _serde::#private::Ok(__ok);
                }
            };
            enum_untagged::deserialize(params, untagged, cattrs, Some(first_attempt))
        }
        None => deserialize_homogeneous_enum(params, variants, cattrs),
    }

带标签的那一段变体按容器的表示(默认外部标签)整体生成一份代码,包进一个闭包,作为 untagged 模板里的 #first_attempt。注意它插在 let __deserializer = ContentRefDeserializer::new(&__content); 之后:带标签的变体也是对 Content 重放,整段输入照样先缓冲。cargo expand 的结果(变体 Num(u32)、Op(char)、#[serde(untagged)] Raw(String)):

let __content = _serde::de::DeserializeSeed::deserialize(
    _serde::__private229::de::ContentVisitor::new(),
    __deserializer,
)?;
let __deserializer = _serde::__private229::de::ContentRefDeserializer::<
    __D::Error,
>::new(&__content);
if let _serde::__private229::Result::<_, __D::Error>::Ok(__ok) = (|| {
    // ...
    #[doc(hidden)]
    const VARIANTS: &'static [&'static str] = &["Num", "Op"];
    _serde::Deserializer::deserialize_enum(
        __deserializer,
        "Token",
        VARIANTS,
        __Visitor {
            marker: _serde::__private229::PhantomData::<Token>,
            lifetime: _serde::__private229::PhantomData,
        },
    )
})() {
    return _serde::__private229::Ok(__ok);
}
if let _serde::__private229::Ok(__ok) = _serde::__private229::Result::map(
    <String as _serde::Deserialize>::deserialize(__deserializer),
    Token::Raw,
) {
    return _serde::__private229::Ok(__ok);
}
_serde::__private229::Err(
    _serde::de::Error::custom(
        "data did not match any variant of untagged enum Token",
    ),
)

src/bin/untagged_variant.rs:

#[derive(Serialize, Deserialize, Debug)]
enum Token {
  Num(u32),
  Op(char),
  #[serde(untagged)]
  Raw(String),
}

fn main() {
  for s in [r#"{"Num":1}"#, r#"{"Op":"+"}"#, r#""abc""#, r#"{"Num":"x"}"#, "1"] {
    println!("{s:<12} => {:?}", serde_json::from_str::<Token>(s));
  }
  for t in [Token::Num(1), Token::Raw("abc".into())] {
    println!("{t:?} => {}", serde_json::to_string(&t).unwrap());
  }
}
{"Num":1}    => Ok(Num(1))
{"Op":"+"}   => Ok(Op('+'))
"abc"        => Ok(Raw("abc"))
{"Num":"x"}  => Err(Error("data did not match any variant of untagged enum Token", line: 0, column: 0))
1            => Err(Error("data did not match any variant of untagged enum Token", line: 0, column: 0))
Num(1) => {"Num":1}
Raw("abc") => "abc"
  • {"Num":"x"} 的标签认出来了,失败在内容上,这个 invalid type 也被丢掉,报的仍是那句话,而且说 Token 是「untagged enum」,虽然它的大部分变体带标签。
  • 序列化一侧没有回退的问题:Num(1) 照外部标签写,Raw 直接写内容。

untagged 变体必须放在最后。检查在 ast.rs 的 index_of_last_tagged_variant:找到最后一个不带 untagged 的变体,它前面出现的 untagged 变体都报错。

#[derive(Deserialize, Debug)]
enum Token {
  #[serde(untagged)]
  Raw(String),
  Num(u32),
}
error: all variants with the #[serde(untagged)] attribute must be placed at the end of the enum
 --> src/bin/untagged_variant_order.rs:6:3
  |
6 |   Raw(String),
  |   ^^^
📝NOTE
冷知识:单个变体的 untagged 是一个手写转换的自动版

这个功能来自 PR #2403「Allowed Enum variants to be individually marked as untagged」,首次发布在 1.0.164(发布说明:「Allowed enum variants to be individually marked as untagged (#2403)」)。在这之前,要达到同样的效果得手写三个类型:一个只含带标签变体的 enum、一个把它当作第一个变体的 untagged enum、再用 #[serde(from = ..., into = ...)] 把原 enum 和后者互转。PR 说明写道:「This can be thought of as applying the following transformation」,并且「The actual implementation generates the implementation directly allowing it to also work efficiently for recursive enums.」变体必须放在最后的限制也写在里面:「in order to keep the implementation strategy consistent with an alternate strategy of attempting to deserialize each variant in order.」同一个版本里还有一个提交「Reuse a single ContentRefDeserializer throughout untagged enum deserialization」(f60324e8),就是前面那个被反复使用的 __deserializer。

动脑筋答案

1                      => Ok(Int(1))
-1                     => Ok(Int(-1))
1.0                    => Ok(Float(1.0))
18446744073709551615   => Ok(Float(1.8446744073709552e19))
  • 1 和 -1:Content 里是 U64(1) 和 I64(-1),i64 的 Visitor 两种都接受,Int 先试先赢。
  • 1.0:Content 里是 F64,i64 不接受浮点数,落到 Float。
  • 18446744073709551615:Content 里是 U64(u64::MAX)。i64 的 visit_u64 检查范围,超出就报错;f64 的 visit_u64 直接转换,得到 1.8446744073709552e19,精度丢了也不报错。调换顺序修好了上一节的 Float(1.0),却又让超出 i64 的整数悄悄变成了浮点数。

小结

  • 生成的代码:ContentVisitor 读成 Content(唯一一个 ?),ContentRefDeserializer 借用它,每个变体一个 if let Ok(..) = .. { return },最后 de::Error::custom(...)。
  • ContentRefDeserializer 是 Copy 的,每次尝试都从树根重读;失败的错误连绑定都没有。
  • unit 变体只认 null;struct 变体没有 visit_seq,读不了数组。
  • 顺序就是声明顺序,第一个 Ok 赢:f64 吃掉整数、少字段的 struct 吃掉多字段的输入,具体的变体要放前面。
  • 报错是 data did not match any variant of untagged enum X,可用 expecting 换掉;顶层没有位置,嵌套时的位置是外层读完的地方。
  • 调试:先读成 serde_json::Value,对每个变体的内容类型单独 from_value,看各自的错误。
  • 单个变体上的 #[serde(untagged)](1.0.164,#2403):带标签的变体先作为整体对 Content 试一次,失败再试 untagged 变体;untagged 变体必须放在最后。

💬 关联讨论

发起讨论
加载中...