如何通过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是否为空:
- 添加Cargo依赖:
serde_with = "3.4.0"
- 实现代码:
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
相关产品推荐
相关产品推荐

