DEBUG.WIKI
腾讯云;让你的想法跑起来;轻量服务器 · 建站 / Docker / API;查看产品 →
晚安云;香港 CN2 ¥15/月
丽萨主机;为跨境业务选好 IP;原生 / 住宅 IP · 按套餐选择;查看 IP →

Serde 相邻标签的空变体,省略 properties 却解析失败

Boolean {} 能读取空 properties 对象,却拒绝省略字段;改成 Boolean 后又拒绝空对象。应先固定协议表示,再处理确需兼容的历史输入。

相邻标签枚举用了 #[serde(tag = "type", content = "properties", rename_all = "snake_case")],Boolean {} 可以读取 {"type":"boolean","properties":{}},省略 properties 后却报 missing field 'properties'。把变体改成 Boolean,省略字段正常,显式空对象又报 expected unit variant。

两种声明并不等价。Boolean {} 是没有字段的结构体变体,仍以对象作为内容;Boolean 是单元变体,内容读取按单元处理。正式 Serde 1.0.229 对缺失 content 的豁免包含单元变体,不包含任意空结构体变体。

先确定输出协议用哪一种

若消息由自己生成,而且协议规定 properties 总是对象,可以保留 Boolean {},让生产方在无属性时也发送 {}。这比在接收方无条件忽略 content 更容易保持一致。

若协议本来规定布尔变体没有内容,采用 Boolean,同时明确拒绝或迁移已有空对象表示。不要只修改 Rust 声明便认定两种历史消息都兼容,尤其是还混有 float、integer 等真正带属性的变体时。

确需读取两种历史表示,只规范已知情况

例如既有接口同时接受省略与空对象,但其他类型必须保持原校验,可以在 JSON 读取入口为已确认的 boolean 消息补上缺失的对象,再交回原 Mapping 类型:

use serde_json::{Map, Value};

fn normalize_boolean(value: &mut Value) {
    let Some(object) = value.as_object_mut() else {
        return;
    };
    let is_boolean = object.get("type").and_then(Value::as_str) == Some("boolean");
    if is_boolean && !object.contains_key("properties") {
        object.insert("properties".into(), Value::Object(Map::new()));
    }
}

这个局部函数假定顶层就是待读取的 Mapping 对象。调用方先把原输入读成 Value,调用函数后再 serde_json::from_value::<Mapping>(value);原类型中的 address 范围、必填字段和各变体属性仍交给正常反序列化。数组或嵌套结构要在应用已知的位置逐项调用,不能递归改写所有恰好叫 type 的对象。

函数不会把显式 null、非空对象或错误类型改成空对象。空结构体对未知字段的默认处理还取决于原模型配置;若协议要求 boolean 的 properties 必须严格为空,应在入口另外验证该对象没有键,不能把默认忽略未知字段当成严格校验。

Value 会消费并重建 JSON 表示,不保留原始排版或重复键信息。签名、逐字转发或重复键校验有要求时,先在原始输入层完成那些检查,再选择有明确协议规则的转换入口。

保留省略、空对象、null、非空对象与其他变体的样本,核对允许的两种输入得到相同业务结果,不允许的形式仍被拒绝。不要为一个空变体打开“忽略所有无效内容”,让本来有意义的属性悄悄消失。