Rust in Everything
基于 serde_json 1.0.154

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 的指南里这一条叫「Maps to Vec of tuples」,Vec<(_, _)> 和 Seq<(_, _)> 等价。代价是 JSON 不再是对象,按键查找的客户端要改。
  • HashMap<DisplayFromStr, _>:键经 Display 写成字符串,读回时经 FromStr 解析,JSON 仍然是对象。字符串格式要自己保证能来回("0,1"),解析失败的报错是 FromStr::Err 的 Display,带位置。
  • Option 键:不要依赖 Some 被透过。要么把键换成非 Option 的类型(None 用一个约定的字符串表示),要么用 Seq<(_, _)>。
  • 浮点键:在插入前过滤掉 NaN 和无穷。
❗IMPORTANT
动脑筋:同一个 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 转字符串)。测试里要放至少一个键。

💬 关联讨论

发起讨论
加载中...