key must be a string
主线位置:序列化主线上写 map 键的那一步。
SerializeMap::serialize_key把键交给MapKeySerializer,而不是普通的Serializer;键的类型走到哪个serialize_*,决定它能不能当 JSON 的键。机制见 序列化一侧 的「MapKeySerializer:键必须写成字符串」。
flowchart TB key["map 的键"] --> mks[MapKeySerializer] mks -->|"str、char、整数、bool"| okk["写成字符串"] mks -->|有限 f64| okk mks -->|"NaN、无穷"| e2["报浮点键须有限"] mks -->|"元组、struct、seq、None"| e1["报键必须是字符串"] e1 --> fix1["Seq:写成键值对数组"] e1 --> fix2["DisplayFromStr:键转字符串"]
MapKeySerializer 的每个方法收什么、整数和布尔键为什么会被加上引号、to_string 和 to_value 为什么用两个不同的 MapKeySerializer,在 序列化一侧 里逐个类型跑过一张表。这一页只看常见的几种写法、修法和检查办法。
本页实验在 M2 Max、macOS、rustc 1.99.0 上运行,依赖 serde = "=1.0.229"(derive)、serde_json = "=1.0.154"、serde_with = "3"(3.24.0)。
现象
Rust 的 map 键可以是任何 Eq + Hash(或 Ord)的类型,类型检查全部通过,错误到运行期才出现。src/bin/km_repro.rs:
use std::collections::{BTreeMap, HashMap};
use serde::Serialize;
#[derive(Serialize, PartialEq, Eq, Hash)]
struct Cell {
row: u32,
col: u32,
}
#[derive(Serialize)]
struct Board {
cells: HashMap<(u32, u32), char>,
}
fn main() {
let mut cells = HashMap::new();
cells.insert((0, 1), 'x');
println!("tuple-key map: {:?}", serde_json::to_string(&Board { cells }));
let mut by_cell = HashMap::new();
by_cell.insert(Cell { row: 0, col: 1 }, 'x');
println!("struct-key map: {:?}", serde_json::to_string(&by_cell));
let mut opt = BTreeMap::new();
opt.insert(Some("a"), 1);
println!("Option key, to_string: {:?}", serde_json::to_string(&opt));
println!("Option key, to_value: {:?}", serde_json::to_value(&opt));
opt.insert(None, 2);
println!("None key, to_string: {:?}", serde_json::to_string(&opt));
// f64 is not Ord/Hash; a Vec of pairs serialized as a map shows the float key path
let floats: Vec<(f64, u32)> = vec![(1.5, 1), (f64::NAN, 2)];
let as_map = serde_json::to_string(&FloatMap(&floats));
println!("f64 keys with NaN: {as_map:?}");
}
struct FloatMap<'a>(&'a [(f64, u32)]);
impl Serialize for FloatMap<'_> {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.collect_map(self.0.iter().map(|(k, v)| (k, v)))
}
}
tuple-key map: Err(Error("key must be a string", line: 0, column: 0))
struct-key map: Err(Error("key must be a string", line: 0, column: 0))
Option key, to_string: Ok("{\"a\":1}")
Option key, to_value: Err(Error("key must be a string", line: 0, column: 0))
None key, to_string: Err(Error("key must be a string", line: 0, column: 0))
f64 keys with NaN: Err(Error("float key must be finite (got NaN or +/-inf)", line: 0, column: 0))
- 元组键和 struct 键:
key must be a string。位置永远是line: 0, column: 0,序列化一侧没有位置可报,报错也不说是哪个字段的 map。 Option键:to_string透过Some写出里面的值,to_value拒绝;一旦有None键,to_string也失败。同一个类型能不能序列化,取决于运行时有没有None。- 浮点键:有限值写成
"1.5",NaN 和无穷单独报一句float key must be finite。作为值的 NaN 会被悄悄写成null(浮点数往返后不相等),作为键却报错。
出错时已经写出去的字节不会收回。src/bin/km_partial.rs:
#[derive(Serialize)]
struct Board {
name: &'static str,
cells: BTreeMap<(u32, u32), char>,
}
fn main() {
let board = Board { name: "demo", cells: BTreeMap::from([((0, 1), 'x')]) };
let mut out = Vec::new();
let r = serde_json::to_writer(&mut out, &board);
println!("result: {r:?}");
println!("bytes already written: {:?}", String::from_utf8(out).unwrap());
}
result: Err(Error("key must be a string", line: 0, column: 0))
bytes already written: "{\"name\":\"demo\",\"cells\":{"
写的是文件或 socket 时,对端拿到的就是半个 JSON。axum 的 Json 响应遇到这个错误时客户端会看到什么,见 (StatusCode, Json) 序列化失败仍返回你给的状态码;那一页建议先 to_value 再放进 Json,对 Option 键要注意:to_string 能写的 Some 键,to_value 反而报错(上面第三、四行)。
json! 宏里写非字符串的键,则在编译期就失败,src/bin/km_json_macro.rs:
use serde_json::json;
fn main() {
let v = json!({ 1: "one" });
println!("{v}");
}
error[E0277]: the trait bound `std::string::String: From<{integer}>` is not satisfied
--> src/bin/km_json_macro.rs:4:11
|
4 | let v = json!({ 1: "one" });
| ^^^^^^^^^^^^^^^^^^^ the trait `From<{integer}>` is not implemented for `std::string::String`
|
= help: `std::string::String` implements trait `From<T>`:
From<&mut str>
From<&std::string::String>
From<&str>
From<Box<str>>
From<Cow<'_, str>>
From<char>
= note: required for `{integer}` to implement `Into<std::string::String>`
= note: this error originates in the macro `$crate::json_internal` which comes from the expansion of the macro `json` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0277`.
error: could not compile `exp-serde-pit3` (bin "km_json_macro") due to 1 previous error
json! 不经过 MapKeySerializer:它把键直接 .into() 成 String 插进 Map(macros.rs 里的 $object.insert(($($key)+).into(), $value)),所以能用的键就是 String 实现了 From 的那几种,char 可以,整数不行。写成 "1"、(2.to_string()) 或 'c' 就能编译,src/bin/km_check.rs 里的 json!({ "1": "a", (2.to_string()): "b", 'c': "d" }) 输出 {"1":"a","2":"b","c":"d"}。
出处
SerializeMap::serialize_key 用 key.serialize(MapKeySerializer { ser: *ser }) 写键。MapKeySerializer 里元组、struct、seq、None 等方法都只有一行,比如 serialize_tuple:
fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple> {
Err(key_must_be_a_string())
}
key_must_be_a_string 构造的是 Error::syntax(ErrorCode::KeyMustBeAString, 0, 0),这就是 line: 0, column: 0 的来历。浮点键在 serialize_f64 里先查 is_finite(),不通过就返回 float_key_must_be_finite()。to_value 用的是 value/ser.rs 里另一个 MapKeySerializer,它的 serialize_some 仍然返回 Err(key_must_be_a_string()),两边为什么不一致在 序列化一侧 的冷知识里。
修法
键本身没有字符串形式时,有两条路:不用 JSON 对象,或者给键定一个字符串形式。src/bin/km_fix.rs:
use std::collections::{BTreeMap, HashMap};
use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use serde_with::{DisplayFromStr, Seq, serde_as};
// 1. map -> JSON array of [key, value] pairs
#[serde_as]
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct Board {
#[serde_as(as = "Seq<(_, _)>")]
cells: BTreeMap<(u32, u32), char>,
}
// 2. key -> JSON string via Display / FromStr
#[derive(PartialEq, Eq, Hash, Debug)]
struct Cell {
row: u32,
col: u32,
}
impl fmt::Display for Cell {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{},{}", self.row, self.col)
}
}
impl FromStr for Cell {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (r, c) = s.split_once(',').ok_or_else(|| format!("no comma in {s:?}"))?;
Ok(Cell {
row: r.parse().map_err(|e| format!("{e}"))?,
col: c.parse().map_err(|e| format!("{e}"))?,
})
}
}
#[serde_as]
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct ByCell {
#[serde_as(as = "HashMap<DisplayFromStr, _>")]
cells: HashMap<Cell, char>,
}
fn main() {
let board = Board { cells: BTreeMap::from([((0, 1), 'x'), ((2, 2), 'o')]) };
let text = serde_json::to_string(&board).unwrap();
let back: Board = serde_json::from_str(&text).unwrap();
println!("Seq<(_, _)>: {text} round trip equal: {}", back == board);
let by_cell = ByCell { cells: HashMap::from([(Cell { row: 0, col: 1 }, 'x')]) };
let text = serde_json::to_string(&by_cell).unwrap();
let back: ByCell = serde_json::from_str(&text).unwrap();
println!("DisplayFromStr: {text} round trip equal: {}", back == by_cell);
println!("bad key: {}", serde_json::from_str::<ByCell>(r#"{"cells":{"01":"x"}}"#).unwrap_err());
}
Seq<(_, _)>: {"cells":[[[0,1],"x"],[[2,2],"o"]]} round trip equal: true
DisplayFromStr: {"cells":{"0,1":"x"}} round trip equal: true
bad key: no comma in "01" at line 1 column 14
Seq<(_, _)>:map 写成[[key, value], ...]数组,键是什么类型都行,读回来是同一个 map。serde_with 的指南里这一条叫「MapstoVecof tuples」,Vec<(_, _)>和Seq<(_, _)>等价。代价是 JSON 不再是对象,按键查找的客户端要改。HashMap<DisplayFromStr, _>:键经Display写成字符串,读回时经FromStr解析,JSON 仍然是对象。字符串格式要自己保证能来回("0,1"),解析失败的报错是FromStr::Err的Display,带位置。Option键:不要依赖Some被透过。要么把键换成非Option的类型(None用一个约定的字符串表示),要么用Seq<(_, _)>。- 浮点键:在插入前过滤掉 NaN 和无穷。
Board,空的时候和有一个格子的时候,to_string 各返回什么?M2 Max、macOS、rustc 1.99.0,src/bin/km_empty.rs:
use std::collections::HashMap;
use serde::Serialize;
#[derive(Serialize)]
struct Board {
cells: HashMap<(u32, u32), char>,
}
fn main() {
let mut board = Board { cells: HashMap::new() };
println!("0 cells: {:?}", serde_json::to_string(&board));
board.cells.insert((0, 1), 'x');
println!("1 cell: {:?}", serde_json::to_string(&board));
}
怎么确认没中招
- 搜
HashMap<(、BTreeMap<(、HashMap<Option<,以及键是自定义 struct / enum(带数据的变体)的 map,看它们会不会被 serde_json 序列化。 - 测试里给这些 map 至少放一个键,
None、NaN 这类边界值也各放一次。只序列化空 map 的测试永远是绿的(见上面的动脑筋)。 - 写文件、写 socket 时,序列化失败后丢弃已经写出的部分,或者先写到
Vec<u8>再整体写出。
动脑筋答案
0 cells: Ok("{\"cells\":{}}")
1 cell: Err(Error("key must be a string", line: 0, column: 0))
- 第一行成功:空 map 只写
{和},serialize_key一次也没被调用,MapKeySerializer根本没有机会拒绝。 - 第二行失败:第一个键
(0, 1)进了MapKeySerializer::serialize_tuple。
所以这个错误不是类型的属性,而是「类型 + 数据」的属性:类型检查不拦,空数据的测试也不拦。
小结
- serde_json 写 map 键时换成
MapKeySerializer:字符串、char、整数、bool、有限浮点、unit 变体、newtype 能当键(数字和布尔加引号),元组、struct、seq、None报key must be a string,NaN / 无穷报float key must be finite。 - 错误在运行期、在第一个键出现时才发生,位置是
line: 0, column: 0;to_writer失败前已经写出的半个 JSON 不会收回。 Some键在to_string里被透过,在to_value里报错;有None键时两边都报错。json!的键直接.into()成String,整数键是编译错误。- 修法:serde_with 的
Seq<(_, _)>(写成键值对数组)或HashMap<DisplayFromStr, _>(键经Display/FromStr转字符串)。测试里要放至少一个键。