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

使用Progenitor生成ZeroTier API客户端时,含anyOf的字段解析失败并抛出InvalidResponsePayload错误

Progenitor生成ZeroTier API客户端时,含anyOf的字段解析失败并抛出InvalidResponsePayload错误

看起来你在使用Progenitor构建ZeroTier API客户端时,被anyOf类型的字段解析问题绊住了——这个场景我之前处理类似的OpenAPI客户端生成时也碰到过,确实挺闹心的。让我们一步步拆解问题,找到解决办法。

问题根源分析

从你给出的报错信息和测试代码来看,核心矛盾出在Progenitor对OpenAPI anyOf的默认处理逻辑和ZeroTier API实际返回的数据格式不匹配:

  1. ZeroTier的/controller/network/{network_id}/member/{node_id}接口返回的ipAssignments字段是纯字符串IP数组(比如["192.168.192.146"])
  2. 但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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 09:38:07