使用Progenitor生成ZeroTier API客户端时,含anyOf的字段解析失败并抛出InvalidResponsePayload错误
看起来你在使用Progenitor构建ZeroTier API客户端时,被anyOf类型的字段解析问题绊住了——这个场景我之前处理类似的OpenAPI客户端生成时也碰到过,确实挺闹心的。让我们一步步拆解问题,找到解决办法。
问题根源分析
从你给出的报错信息和测试代码来看,核心矛盾出在Progenitor对OpenAPI anyOf的默认处理逻辑和ZeroTier API实际返回的数据格式不匹配:
- ZeroTier的
/controller/network/{network_id}/member/{node_id}接口返回的ipAssignments字段是纯字符串IP数组(比如["192.168.192.146"]) - 但Progenitor根据OpenAPI的
anyOf定义,生成了带#[serde(flatten)]属性的ControllerNetworkMemberIpAssignmentsItem结构体——这个属性会要求序列化/反序列化时处理结构体/映射类型,而不是纯字符串,所以才会出现:- 反序列化服务器返回时,报错
invalid type: string "192.168.192.146", expected struct ControllerNetworkMemberIpAssignmentsItem - 手动序列化结构体时,报错
can only flatten structs and maps (got a string)
- 反序列化服务器返回时,报错
解决方案
针对这个问题,有几个可行的解决方向,按推荐优先级排序:
1. 调整OpenAPI规范,让Progenitor生成兼容的枚举类型
Progenitor对oneOf的支持比anyOf更友好,能生成带#[serde(untagged)]的枚举类型,完美适配ZeroTier的混合返回格式(纯字符串IP或结构化IP对象)。
修改你的api.yaml中ControllerNetworkMemberIpAssignmentsItem的定义:
ControllerNetworkMemberIpAssignmentsItem: oneOf: - type: string format: ipv4 - type: object properties: ip: type: string format: ipv4 # 保留原OpenAPI中定义的其他结构体字段 # 无需 discriminator,untagged 枚举会自动匹配字符串或对象
重新生成API客户端后,Progenitor会生成一个untagged枚举,既能反序列化ZeroTier返回的纯字符串IP,也能处理结构化的IP对象,序列化也能正常工作。
2. 手动修改生成的Rust类型(临时应急方案)
如果暂时无法修改OpenAPI规范,可以手动替换Progenitor生成的ControllerNetworkMemberIpAssignmentsItem类型为untagged枚举:
// 替换生成的结构体为以下枚举 #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] #[serde(untagged)] pub enum ControllerNetworkMemberIpAssignmentsItem { Ip(String), StructuredAssignment { ip: crate::types::IPv4, // 原结构体中的其他字段,比如 subtype_0/subtype_1 对应的实际字段名 }, }
这个枚举会自动尝试先把数据反序列化为字符串,失败再尝试结构化对象,完美适配ZeroTier的返回格式,序列化也能正常输出字符串或对象。
3. 为生成的结构体自定义Serde序列化逻辑
如果必须保留结构体形式,可以为ControllerNetworkMemberIpAssignmentsItem手动实现Deserialize和Serialize trait,处理纯字符串和结构体的转换:
use std::str::FromStr; use serde::{Deserialize, Deserializer, Serialize, Serializer}; impl<'de> Deserialize<'de> for ControllerNetworkMemberIpAssignmentsItem { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de> { // 先尝试反序列化为纯字符串IP let ip_str = match String::deserialize(deserializer) { Ok(s) => s, // 失败则尝试反序列化为结构化对象 Err(_) => { let obj: serde_json::Value = Deserialize::deserialize(deserializer)?; return Ok(Self { subtype_0: obj.get("ip") .and_then(|v| v.as_str()) .map(|s| crate::types::IPv4::from_str(s).map_err(serde::de::Error::custom)) .transpose()?, subtype_1: obj.get("subtype_1") .and_then(|v| v.as_str()) .map(|s| /* 转换为 subtype_1 的对应类型 */) .transpose()?, }); } }; Ok(Self { subtype_0: Some(crate::types::IPv4::from_str(&ip_str).map_err(serde::de::Error::custom)?), subtype_1: None, }) } } impl Serialize for ControllerNetworkMemberIpAssignmentsItem { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer { // 序列化时,如果只有 subtype_0 有值,直接输出字符串,否则输出结构体 if let (Some(ip), None) = (&self.subtype_0, &self.subtype_1) { ip.to_string().serialize(serializer) } else { // 用临时结构体序列化 #[derive(Serialize)] struct Temp { ip: Option<crate::types::IPv4>, subtype_1: Option</* 对应类型 */>, } Temp { ip: self.subtype_0.clone(), subtype_1: self.subtype_1.clone(), }.serialize(serializer) } } }
这个方案比较繁琐,但适合无法修改OpenAPI规范的场景。
验证修复效果
修改后,你可以重新运行测试代码:
use std::str::FromStr; use progenitor::generate_api; // 确保生成的类型是枚举或者自定义了Serde逻辑的结构体 generate_api!("api.yaml"); fn main() { let b_items = vec![ ControllerNetworkMemberIpAssignmentsItem::Ip("127.0.0.1".to_string()), ]; println!("{}", serde_json::to_string(&b_items).unwrap()); // 应该正常输出 ["127.0.0.1"] }
同时调用ZeroTier的network_member_get_network_member函数时,也能正常反序列化服务器返回的数据了。
内容来源于stack exchange

