You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过Serde处理REST API中全字段为null的结构体反序列化?

Octoprint Rust客户端的Serde反序列化适配方案

问题背景

我在开发Octoprint的Rust客户端库时,遇到了API响应与文档不一致的问题:

  • 按照API规范的multiplicity属性,我定义了JobInfo结构体,仅给部分字段标记为Option
  • 但当打印机离线时,API返回的job是一个所有字段均为null的对象,而非规范中预期的null值
  • 直接将JobInformationResponse中的job字段改为Option<JobInfo>无法解决问题——Serde不会把全null的对象反序列化为None

相关结构体定义:

#[derive(Serialize, Deserialize, Debug)]
pub struct JobInfo {
    /// 当前打印任务的目标文件
    pub file: FileInfo,
    /// 文件的预计打印时间(秒)
    #[serde(rename = "estimatedPrintTime")]
    pub estimated_print_time: Option<f64>,
    /// 文件上次打印的时间(秒)
    #[serde(rename = "lastPrintTime")]
    pub last_print_time: Option<f64>,
    /// 打印任务的预计耗材使用信息
    pub filament: Option<Filament>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct JobInformationResponse {
    /// 当前打印任务的目标信息
    job: JobInfo,
    /// 当前打印任务的进度信息
    progress: ProgressInfo,
    /// 任务或连接的当前状态文本描述
    state: String,
    /// 任务或连接的错误信息(仅出错时设置)
    error: Option<String>,
}

离线状态下的API响应片段:

{
  "job": {
    "file": null,
    "filepos": null,
    "printTime": null,
    ...
  },
  ...
  "state": "Offline"
}

需求是:当job对象的所有字段均为null时,自动将其反序列化为None,同时避免给JobInfo内部所有字段都套Option。


解决方案

方法1:自定义Option<JobInfo>的反序列化逻辑(无第三方依赖)

直接为Option<JobInfo>实现自定义反序列化,先将JSON解析为通用Value,检查所有字段是否为null,再决定返回None或反序列化为JobInfo:

use serde::{Deserialize, Deserializer};
use serde_json::Value;

// 基础结构体定义(根据实际项目调整)
#[derive(Serialize, Deserialize, Debug)]
pub struct FileInfo {
    pub name: Option<String>,
    pub path: Option<String>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct Filament {
    pub length: Option<f64>,
    pub volume: Option<f64>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct JobInfo {
    pub file: FileInfo,
    #[serde(rename = "estimatedPrintTime")]
    pub estimated_print_time: Option<f64>,
    #[serde(rename = "lastPrintTime")]
    pub last_print_time: Option<f64>,
    pub filament: Option<Filament>,
}

// 为Option<JobInfo>实现自定义反序列化
impl<'de> Deserialize<'de> for Option<JobInfo> {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let raw_value = Value::deserialize(deserializer)?;

        // 检查是否是全null的对象
        if let Value::Object(obj) = &raw_value {
            let all_fields_null = obj.values().all(|v| v.is_null());
            if all_fields_null {
                return Ok(None);
            }
        }

        // 正常反序列化为JobInfo
        let job_info = JobInfo::deserialize(raw_value)?;
        Ok(Some(job_info))
    }
}

// 更新响应结构体,将job改为Option<JobInfo>
#[derive(Serialize, Deserialize, Debug)]
pub struct JobInformationResponse {
    pub job: Option<JobInfo>,
    pub progress: ProgressInfo,
    pub state: String,
    pub error: Option<String>,
}

// 假设的ProgressInfo结构体
#[derive(Serialize, Deserialize, Debug)]
pub struct ProgressInfo {
    pub completion: Option<f64>,
    pub print_time: Option<f64>,
}

方法2:使用serde_with库简化实现

如果允许引入第三方库,serde_with的DefaultOnNull特性可以将null字段自动转换为结构体的默认实例,再通过自定义方法判断整个JobInfo是否为空:

  1. 添加Cargo依赖:
serde_with = "3.4.0"
  1. 实现代码:
use serde::{Deserialize, Deserializer};
use serde_with::serde_as;

// 基础结构体定义
#[derive(Serialize, Deserialize, Debug, Default)]
pub struct FileInfo {
    pub name: Option<String>,
    pub path: Option<String>,
}

#[derive(Serialize, Deserialize, Debug, Default)]
pub struct Filament {
    pub length: Option<f64>,
    pub volume: Option<f64>,
}

#[serde_as]
#[derive(Serialize, Deserialize, Debug)]
pub struct JobInfo {
    // 将null的file字段转换为FileInfo的默认实例
    #[serde_as(as = "DefaultOnNull")]
    pub file: FileInfo,
    #[serde(rename = "estimatedPrintTime")]
    pub estimated_print_time: Option<f64>,
    #[serde(rename = "lastPrintTime")]
    pub last_print_time: Option<f64>,
    pub filament: Option<Filament>,
}

impl JobInfo {
    // 判断当前实例是否所有字段均为空
    fn is_all_empty(&self) -> bool {
        let file_empty = self.file.name.is_none() && self.file.path.is_none();
        let times_empty = self.estimated_print_time.is_none() && self.last_print_time.is_none();
        let filament_empty = self.filament.is_none();

        file_empty && times_empty && filament_empty
    }
}

// 为Option<JobInfo>实现反序列化
impl<'de> Deserialize<'de> for Option<JobInfo> {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let job_info = JobInfo::deserialize(deserializer)?;
        Ok(if job_info.is_all_empty() { None } else { Some(job_info) })
    }
}

// 更新响应结构体
#[derive(Serialize, Deserialize, Debug)]
pub struct JobInformationResponse {
    pub job: Option<JobInfo>,
    pub progress: ProgressInfo,
    pub state: String,
    pub error: Option<String>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct ProgressInfo {
    pub completion: Option<f64>,
    pub print_time: Option<f64>,
}

方案对比

  • 方法1无需额外依赖,适合轻量场景,但如果JobInfo新增字段,需要同步更新全null判断逻辑
  • 方法2逻辑更清晰,DefaultOnNull自动处理单个字段的null转换,新增字段时只需在is_all_empty中补充判断即可

内容的提问来源于stack exchange,提问作者Brandon Piña

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 13:45:15