如何使用Serde实现结构体两个互斥字段仅允许其一存在的反序列化
实现方案
有两种常用的实现方式,可根据你的业务需求选择:
方案一:不修改现有结构体结构,基于try_from做校验
如果你需要保留MyStruct现有field_1、field_2两个可选字段的结构,可以用serde的try_from属性,在反序列化完成后强制校验两个字段的互斥逻辑:
use serde::{Serialize, Deserialize}; use std::convert::TryFrom; // 中间结构体,用来做原始反序列化,字段和目标结构体完全一致 #[derive(Deserialize)] struct RawMyStruct { field_1: Option<usize>, field_2: Option<usize>, other_field: String, } #[derive(Deserialize, Debug)] #[serde(try_from = "RawMyStruct")] struct MyStruct { field_1: Option<usize>, field_2: Option<usize>, other_field: String, } impl TryFrom<RawMyStruct> for MyStruct { // 示例用简单字符串作为错误类型,生产环境可自定义符合std::error::Error的错误类型获得更详细的报错信息 type Error = &'static str; fn try_from(value: RawMyStruct) -> Result<Self, Self::Error> { match (value.field_1.is_some(), value.field_2.is_some()) { (true, true) => Err("不能同时存在field_1和field_2字段"), (false, false) => Err("必须存在field_1、field_2中的一个字段"), _ => Ok(Self { field_1: value.field_1, field_2: value.field_2, other_field: value.other_field, }) } } }
该方案优势是对上层使用方完全透明,结构体对外暴露的字段和原始定义完全一致,不需要修改后续的业务使用逻辑。
方案二:基于枚举优化类型设计,从类型层面保证互斥
如果你可以调整结构体的结构,更推荐用Rust的枚举类型把两个互斥字段抽象成统一的类型,天然在反序列化阶段就保证互斥,不需要额外编写校验逻辑:
use serde::{Serialize, Deserialize}; #[derive(Deserialize, Debug)] #[serde(rename_all = "snake_case")] enum MyField { Field1(usize), Field2(usize), } #[derive(Deserialize, Debug)] struct MyStruct { // 将枚举的字段展开到结构体上层,直接匹配输入的field_1、field_2字段 #[serde(flatten)] my_field: MyField, other_field: String, } // 可根据业务需要添加便捷访问方法,兼容原来的字段访问逻辑 impl MyStruct { pub fn get_field_value(&self) -> usize { match &self.my_field { MyField::Field1(v) => *v, MyField::Field2(v) => *v, } } pub fn is_field1(&self) -> bool { matches!(&self.my_field, MyField::Field1(_)) } pub fn is_field2(&self) -> bool { matches!(&self.my_field, MyField::Field2(_)) } }
该方案的原理是通过#[serde(flatten)]把枚举的字段展开到MyStruct的上层,枚举变体经过snake_case重命名后刚好对应field_1、field_2两个字段,serde会自动匹配输入中存在的字段,两个都存在或者都不存在时都会直接返回反序列化错误,符合类型安全设计,后续使用时也不用担心忘记校验两个字段的存在情况。
内容的提问来源于stack exchange,提问作者cactus
相关产品推荐
相关产品推荐

