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。整数的每一对转换写在标准库类型的实现。
u32 的 Visitor1.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。
deserialize_any 换成 deserialize_u32src/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, _>修。