Rust in Everything
基于 serde 1.0.229

invalid type: string "1", expected u32

主线位置:反序列化主线的第一步。u32 的 Deserialize 调 deserialize_u32 提示格式「这里要一个 u32」,serde_json 照提示只解析数字,看到 " 就在格式里报错,u32 的 Visitor 一个方法也没被调到。同样的报错还会从缓冲主线冒出来:数字键 map 进了 Content 之后,键成了字符串。

flowchart TB
  hint["deserialize_u32 提示"] --> peek{"下一个字节"}
  peek -->|数字| visit["visit_u64"]
  visit -->|超出范围| val["invalid value"]
  peek -->|"引号"| pit["peek_invalid_type"]
  pit --> err["invalid type: string"]
  fix["改成 deserialize_any"] --> both{"数字还是字符串"}
  both --> vu["visit_u64"]
  both --> vs["visit_str 再 parse"]

deserialize_* 为什么只是提示、serde_json 怎么照提示读,在 Deserialize、Deserializer 与 Visitor;invalid type 和 invalid value 这两半句怎么拼出来,在错误。这一页只讲碰到之后怎么办。

本页实验在 M2 Max、macOS、rustc 1.99.0 上运行,依赖 serde = "=1.0.229"(derive)、serde_json = "=1.0.154"、serde_with = "3"(解析到 3.24.0)。

现象

一个上游服务把数量写成了 "1"。src/bin/invalid_type.rs 的第一部分:

#[derive(Deserialize, Debug)]
struct Plain {
  qty: u32,
}

#[derive(Deserialize, Debug)]
struct Small {
  x: u8,
}

fn show<T: DeserializeOwned + fmt::Debug>(label: &str, s: &str) {
  match serde_json::from_str::<T>(s) {
    Ok(v) => println!("{label:<14} {s:<26} Ok({v:?})"),
    Err(e) => println!("{label:<14} {s:<26} Err({e})"),
  }
}

fn main() {
  show::<Plain>("Plain", r#"{"qty":"1"}"#);
  show::<Plain>("Plain", r#"{"qty":1}"#);
  // ...
  show::<Small>("u8", r#"{"x":300}"#);
  show::<Small>("u8", r#"{"x":1.0}"#);
  show::<Small>("u8", r#"{"x":-1}"#);
  show::<Small>("u8", r#"{"x":true}"#);
}
Plain          {"qty":"1"}                Err(invalid type: string "1", expected u32 at line 1 column 10)
Plain          {"qty":1}                  Ok(Plain { qty: 1 })
-- neighbours
u8             {"x":300}                  Err(invalid value: integer `300`, expected u8 at line 1 column 8)
u8             {"x":1.0}                  Err(invalid type: floating point `1.0`, expected u8 at line 1 column 8)
u8             {"x":-1}                   Err(invalid value: integer `-1`, expected u8 at line 1 column 7)
u8             {"x":true}                 Err(invalid type: boolean `true`, expected u8 at line 1 column 9)

这几条报错是一家人,区别在前半句:

报错意思例子
invalid type: string "1"输入的种类不对:要数字,来了字符串"1" → u32
invalid type: floating point \1.0``种类不对:要整数,来了浮点,值是不是整数不看1.0 → u8
invalid value: integer \300``种类对,值不在范围里300、-1 → u8

后半句 expected u32 来自 u32 那个 Visitor 的 expecting;位置 at line 1 column 10 是 serde_json 补的,指向出错的值刚读完的地方。

出处

deserialize_u32 在 serde_json 里是一行宏:deserialize_number!(deserialize_u32),展开后调 deserialize_number:

        let value = match peek {
            b'-' => {
                self.eat_char();
                tri!(self.parse_integer(false)).visit(visitor)
            }
            b'0'..=b'9' => tri!(self.parse_integer(true)).visit(visitor),
            _ => Err(self.peek_invalid_type(&visitor)),
        };

不是 - 或数字,就交给 peek_invalid_type。它把这个值完整解析一遍,只为了在报错里写出它是什么:

            b'"' => {
                self.eat_char();
                self.scratch.clear();
                match self.read.parse_str(&mut self.scratch) {
                    Ok(s) => de::Error::invalid_type(Unexpected::Str(&s), exp),
                    Err(err) => return err,
                }
            }

exp 就是传进来的 &visitor,它只被用来打印「expected u32」,visit_str 根本没被调用。所以即使你的 Visitor 实现了 visit_str,只要它是经 deserialize_u32 进来的,"1" 还是这一句报错(页末的动脑筋)。

另外两种走的是 Visitor:300 解析成整数,visit_u64(300) 进了 u8 的 uint_to_self!,try_from 失败报 invalid_value(Unexpected::Unsigned(..));1.0 解析成浮点,u8 的 Visitor 没有 visit_f64,落到默认实现报 invalid_type。整数的每一对转换写在标准库类型的实现。

📝NOTE
冷知识:serde_json 1.0.8 起,字符串不会再交给 u32 的 Visitor

1.0.8 之前,serde_json 的大多数 deserialize_* 都转给 deserialize_any,看到 " 就调 visit_borrowed_str 或 visit_str,由 Visitor 的默认实现报 invalid_type。这样每个 Visitor 的每个 visit_* 都会被单态化一份。PR #389「Implement all the deserializer hints」改成照提示读:「If the Deserialize impl hints that a boolean is required by invoking deserialize_bool, we don't also need to instantiate its visit_u64 and visit_i64 ... Instead we render the invalid_type error in a central place in the serde_json Deserializer that is instantiated just once.」,提交说明里的数字是「Improves compile time of json-benchmark by 30%」。这个中心位置就是上面的 peek_invalid_type。

v1.0.8 的发布说明专门写了兼容性提示:「This does mean that code relying on the deserialize hint to be wrong may no longer deserialize successfully.」——提示说 u32、Visitor 却想收字符串的代码,从这个版本起读不到字符串了。

修法

字段的类型不改,改它怎么读。src/bin/invalid_type.rs 里四种写法:

#[serde_as]
#[derive(Deserialize, Debug)]
struct WithDisplay {
  #[serde_as(as = "DisplayFromStr")]
  qty: u32,
}

#[serde_as]
#[derive(Deserialize, Debug)]
struct WithPickFirst {
  #[serde_as(as = "PickFirst<(_, DisplayFromStr)>")]
  qty: u32,
}

// Accept both 1 and "1": the visitor answers visit_u64 and visit_str.
fn u32_or_string<'de, D: Deserializer<'de>>(d: D) -> Result<u32, D::Error> {
  struct V;
  impl<'de> Visitor<'de> for V {
    type Value = u32;
    fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
      f.write_str("a u32 or a string containing one")
    }
    fn visit_u64<E: de::Error>(self, v: u64) -> Result<u32, E> {
      u32::try_from(v).map_err(|_| E::invalid_value(de::Unexpected::Unsigned(v), &self))
    }
    fn visit_str<E: de::Error>(self, v: &str) -> Result<u32, E> {
      v.parse().map_err(|_| E::invalid_value(de::Unexpected::Str(v), &self))
    }
  }
  d.deserialize_any(V)
}

#[derive(Deserialize, Debug)]
struct WithFn {
  #[serde(deserialize_with = "u32_or_string")]
  qty: u32,
}

#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum NumOrStr {
  Num(u32),
  Str(String),
}

// Let an untagged helper take either shape, then convert.
fn via_untagged<'de, D: Deserializer<'de>>(d: D) -> Result<u32, D::Error> {
  match NumOrStr::deserialize(d)? {
    NumOrStr::Num(n) => Ok(n),
    NumOrStr::Str(s) => s.parse().map_err(de::Error::custom),
  }
}

#[derive(Deserialize, Debug)]
struct WithUntagged {
  #[serde(deserialize_with = "via_untagged")]
  qty: u32,
}
DisplayFromStr {"qty":"1"}                Ok(WithDisplay { qty: 1 })
DisplayFromStr {"qty":1}                  Err(invalid type: integer `1`, expected a string at line 1 column 8)
PickFirst      {"qty":"1"}                Ok(WithPickFirst { qty: 1 })
PickFirst      {"qty":1}                  Ok(WithPickFirst { qty: 1 })
with fn        {"qty":"1"}                Ok(WithFn { qty: 1 })
with fn        {"qty":1}                  Ok(WithFn { qty: 1 })
with fn        {"qty":"x"}                Err(invalid value: string "x", expected a u32 or a string containing one at line 1 column 10)
untagged       {"qty":"1"}                Ok(WithUntagged { qty: 1 })
untagged       {"qty":1}                  Ok(WithUntagged { qty: 1 })
untagged       {"qty":"x"}                Err(invalid digit found in string at line 1 column 11)
写法"1"1代价
DisplayFromStr读拒绝只收字符串,把坑翻了个面;序列化时也写成 "1"
PickFirst<(_, DisplayFromStr)>读读serde_with 3.24.0 先把值读进它自己的一份 Content(deserialize_any),再逐个尝试,和 Content 一样多一次缓冲
手写 deserialize_with读读必须调 deserialize_any;报错由自己的 expecting 决定
untagged 辅助类型读读经 Content 缓冲;"x" 的报错是 parse 的原文,看不出是哪个字段该是数字

手写版本里的关键是最后一行 d.deserialize_any(V)。deserialize_any 让 serde_json 看着输入决定调 visit_u64 还是 visit_str,两条路都通到 V。除了 DisplayFromStr(它调的是 deserialize_str),另外三种都依赖 deserialize_any,只适用于 JSON 这类自描述格式;bincode 一类为什么不行,见 Bincode does not support the serde::Deserializer::deserialize_any method。

❗IMPORTANT
动脑筋:把 deserialize_any 换成 deserialize_u32

src/bin/hint_wrong.rs:同一个 V(visit_u64 和 visit_str 都实现了),一个函数调 d.deserialize_u32(V),另一个调 d.deserialize_any(V),各挂在一个 deserialize_with 字段上:

fn hint_u32<'de, D: Deserializer<'de>>(d: D) -> Result<u32, D::Error> {
  d.deserialize_u32(V)
}

fn hint_any<'de, D: Deserializer<'de>>(d: D) -> Result<u32, D::Error> {
  d.deserialize_any(V)
}

fn main() {
  for s in [r#"{"qty":"1"}"#, r#"{"qty":1}"#] {
    println!("deserialize_u32 {s:<12} {:?}", serde_json::from_str::<HintU32>(s).map_err(|e| e.to_string()));
    println!("deserialize_any {s:<12} {:?}", serde_json::from_str::<HintAny>(s).map_err(|e| e.to_string()));
  }
  let v = serde_json::json!({"qty": "1"});
  println!("from_value u32  {:?}", serde_json::from_value::<HintU32>(v).map_err(|e| e.to_string()));
}

deserialize_u32 那一行读 {"qty":"1"},是 Ok(1) 还是报错?报错的话,expected 后面是 u32 还是别的?换成 from_value 呢?答案在页末。

只在内部标签和 flatten 里出现:数字键的 map

同一个错误还有一个来源,输入里根本没有写错的字符串。JSON 对象的键只能是字符串,HashMap<u32, bool> 能直接读 {"1":true},是因为 serde_json 的 MapKey 收到 deserialize_u32 提示时走 deserialize_numeric_key!,去掉引号按数字解析。把这个 struct 放进内部标签或 flatten:

#[derive(Deserialize, Debug)]
struct Keys {
  m: HashMap<u32, bool>,
}

#[derive(Deserialize, Debug)]
#[serde(tag = "t")]
enum Tagged {
  V(Keys),
}

#[derive(Deserialize, Debug)]
struct Outer {
  id: u32,
  #[serde(flatten)]
  keys: Keys,
}

#[serde_as]
#[derive(Deserialize, Debug)]
struct KeysFixed {
  #[serde_as(as = "HashMap<DisplayFromStr, _>")]
  m: HashMap<u32, bool>,
}

#[derive(Deserialize, Debug)]
#[serde(tag = "t")]
enum TaggedFixed {
  V(KeysFixed),
}
-- numeric map keys
plain          {"m":{"1":true}}           Ok(Keys { m: {1: true} })
tagged         {"t":"V","m":{"1":true}}   Err(invalid type: string "1", expected u32)
flatten        {"id":1,"m":{"1":true}}    Err(invalid type: string "1", expected u32 at line 1 column 23)
tagged fixed   {"t":"V","m":{"1":true}}   Ok(V(KeysFixed { m: {1: true} }))
plain fixed    {"m":{"1":true}}           Ok(KeysFixed { m: {1: true} })

内部标签和 flatten 先用 deserialize_any 把对象读进 Content,键 "1" 在树里就是一个字符串;重放时 u32 的 deserialize_u32 落到 ContentDeserializer::deserialize_integer,只认 Content::U8 到 Content::I64,其他一律 invalid_type。serde_json 那段「把带引号的键当数字」的特殊处理在缓冲时已经错过了。这是 Content 「丢掉了什么」里的 #1183,写作时仍未关闭。

两行报错还有一个差别:内部标签那行没有位置,flatten 那行有,column 23 是整个对象的结尾。内部标签的重放发生在 serde_json 的 deserialize_any 返回之后,没人再补位置;flatten 的重放还在外层 deserialize_map 里面,serde_json 返回前给它补上了当前位置,但那已经不是出错的那个键了(原因同样在 Content)。

修法是让键按字符串读,再自己转:HashMap<DisplayFromStr, _> 对键用 deserialize_str,Content::String 正好能给它;读完 parse 成 u32。不经缓冲时它也照样能用(plain fixed)。

怎么确认没中招

  • 给每个对外的 struct 准备一份「上游真实发来的」样例,用 from_str 跑一遍。这个坑只取决于输入长什么样,单元测试里自己 to_string 再 from_str 永远是数字,测不出来。
  • 报错里的位置也是线索。 带 at line .. column .. 的是直接读时的报错,位置就是那个值;没有位置,或者位置落在对象结尾,说明它是从缓冲(内部标签、untagged、flatten)里冒出来的。
  • 数字键 map 搜一遍:HashMap<u32, _>、BTreeMap<i64, _> 一类的字段,所在的类型有没有被放进 #[serde(tag = ...)] 的变体或 #[serde(flatten)] 字段。有就换成 HashMap<DisplayFromStr, _>,或者键改成 String。
  • 手写的 deserialize_with 想收两种输入,就调 deserialize_any。 调 deserialize_u32 而 Visitor 写了 visit_str,visit_str 永远不会被 serde_json 调到。

动脑筋答案

deserialize_u32 {"qty":"1"}  Err("invalid type: string \"1\", expected a u32 or a string containing one at line 1 column 10")
deserialize_any {"qty":"1"}  Ok(HintAny { qty: 1 })
deserialize_u32 {"qty":1}    Ok(HintU32 { qty: 1 })
deserialize_any {"qty":1}    Ok(HintAny { qty: 1 })
from_value u32  Err("invalid type: string \"1\", expected a u32 or a string containing one")
  • deserialize_u32 读 "1" 报错,尽管 V 实现了 visit_str。serde_json 照提示进 deserialize_number,看到 " 就进 peek_invalid_type,V 只被拿去打印 expecting,所以后半句是「a u32 or a string containing one」,不是 u32。这正是 v1.0.8 发布说明里那句「relying on the deserialize hint to be wrong」。
  • deserialize_any 两种输入都行:serde_json 看着输入调 visit_str 或 visit_u64。
  • from_value 也一样拒绝。 默认 feature 下,Value 作为 Deserializer 时数字类的提示只接受 Value::Number,Value::String 同样报 invalid_type,只是没有位置(Value 里没有行列号)。

小结

  • invalid type 是种类不对(字符串、浮点、布尔对上整数),invalid value 是种类对、值越界;后半句来自 Visitor::expecting,位置由 serde_json 补。
  • "1" 读成 u32 的报错在 serde_json 里产生:deserialize_number 看到引号直接 peek_invalid_type,不调任何 visit_*。这是 serde_json 1.0.8(PR #389)为编译时间做的改动。
  • 放宽的办法:DisplayFromStr(只收字符串)、PickFirst<(_, DisplayFromStr)>(两种都收,多一次缓冲)、手写 deserialize_with 并调 deserialize_any、untagged 辅助类型(报错变差)。都只适用于自描述格式。
  • 数字键 map 放进内部标签或 flatten,键在 Content 里成了字符串,重放时报同样的错;用 HashMap<DisplayFromStr, _> 修。

💬 关联讨论

发起讨论
加载中...