DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
PanstarCloud:部署应用,接入 AI,云服务器与 AI 网关
阿里云;从建站到云上业务;云服务器 · 数据库 · 对象存储;查看产品 →

serde_json 的 flatten 扩展字段报 invalid type: newtype struct

已知 JSON 字段能解析,一旦出现未知键就报错,检查 flatten 映射里的 RawValue。需要普通值时用 Value;需要保留原始片段时,在最外层直接读取原始值映射。

同一份 Rust 结构体可以读取所有已知字段,JSON 多出一个未知键,就报 invalid type: newtype struct, expected any valid JSON value。如果结构体用 #[serde(flatten)] 把未知字段收进映射,而映射值是 RawValue,先检查这个组合。

具体故障出现在 serde_json 1.0.151。空对象、只有已知字段的对象都可以通过;真正有字段进入 BTreeMap<&str, &RawValue> 时才失败。给普通、明确命名的字段使用 RawValue 可以正常工作,不表示 flatten 的中间路径也能保留同样的信息。

未知字段经过了另一层反序列化

RawValue 需要从 JSON 解析器取得一个值对应的原始 JSON 文本。flatten 则先把尚未归属的字段收进 Serde 的通用内容缓冲,再让扩展映射消费。这个缓冲并不等同于仍握有原始输入位置的 JSON 解析器,因此 RawValue 所需的专用读取无法按原路径完成。

这里的 newtype struct 是反序列化机制中的类型提示,不说明输入必须改成某种“newtype JSON”。JSON 仍然只有对象、数组、字符串等普通值。不要为了消除提示就在网络协议里增加一个包装对象。

截至 2026 年 10 月 24 日,不能把升级到正式 serde_json 1.0.154 当成这一组合已经修复的依据。先确定扩展字段究竟需要普通 JSON 值,还是必须保留原始片段,再改接收方式。

不要求保留原始写法时,使用 Value

如果扩展字段只用于检查和业务处理,可以直接用 serde_json::Value 承接。下面是启用了 Serde derive 的局部类型声明:

use serde::Deserialize;
use serde_json::Value;
use std::collections::BTreeMap;

#[derive(Deserialize)]
struct Input {
    isp: Option<String>,
    #[serde(flatten)]
    extra: BTreeMap<String, Value>,
}

extra 会保留未知键及其 JSON 值,字段为 null 时仍是 Value::Null。键改成拥有所有权的 String,也避免让扩展字段的生命周期依赖调用方持有输入字符串。

Value 不是原始字节容器。数字表示、空白、对象键顺序等内容重新序列化后可能变化;需要签名校验、原样转发或保留数字原始写法时,不能把 Value 转回字符串当成原始输入。

需要原始片段时,从整个对象直接读取

另一种入口是先把顶层对象解码成 BTreeMap<String, Box<RawValue>>,让每个值直接经过 JSON 解析器,再移除和解析已知字段。使用 RawValue 需开启 serde_json 的 raw_value feature。下面的函数只处理 isp,其他键留在返回的映射中:

use serde_json::value::RawValue;
use std::collections::BTreeMap;

type RawFields = BTreeMap<String, Box<RawValue>>;

fn split_fields(input: &str) -> serde_json::Result<(Option<String>, RawFields)> {
    let mut fields: RawFields = serde_json::from_str(input)?;
    let isp = fields
        .remove("isp")
        .map(|raw| serde_json::from_str::<Option<String>>(raw.get()))
        .transpose()?
        .flatten();
    Ok((isp, fields))
}

这里约定 isp 缺失或为 null 都返回 None,非字符串的非空值报错。实际有其他已知字段时,要逐个按原契约解析,不能漏掉它们的校验。该方案保留的是每个值的原始 JSON 片段,不保留整个对象的键顺序和排版;重复键也会被映射覆盖。需要整个消息逐字节保存时,仍须保留原始输入,并单独决定重复键策略。

回查空扩展映射、未知数字、字符串、数组和 null,以及已知字段的错误类型。若改成 Value 后参与了签名、缓存键或转发,应一起核对输出变化,不能只用“解析成功”判断兼容性。