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

如何用Serde将API分页JSON反序列化为含Option<Url>的Rust结构体

问题描述

我从分页API获取数据,需要将返回的分页JSON反序列化为自定义Rust结构体。API返回的JSON格式如下:

{
    "offset":0,
    "max":20,
    "size":20,
    "links":[
        {
            "rel":"next",
            "uri":"https://.../api/v1/endpoint?offset=20"
        },
        {
            "rel":"prev",
            "uri":"https://.../api/v1/endpoint"
        }
    ]
}

其中offset、max、size字段始终存在,links数组包含next、prev链接中的一个或两个。

原本的反序列化结构体比较复杂,现在我只关心next链接,希望简化为如下结构体:

#[derive(Deserialize, Debug)]
pub struct Pagination{
    next: Option<Url>,
    max: i64,
    offset: i64,
    size: i64,
}

需要编写自定义反序列化逻辑,将JSON中的links数组转换为结构体中Option<Url>类型的next字段。

解决方案

这里提供两种实现方式,都能满足需求:

方式一:为结构体实现完整自定义反序列化

直接为Pagination实现Deserialize trait,通过临时结构体匹配原始JSON结构,再提取需要的next链接:

use serde::Deserialize;
use url::Url;

#[derive(Debug)]
pub struct Pagination {
    pub next: Option<Url>,
    pub max: i64,
    pub offset: i64,
    pub size: i64,
}

impl<'de> Deserialize<'de> for Pagination {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        // 临时结构体,完全匹配原始JSON的字段结构
        #[derive(Deserialize)]
        struct RawPagination {
            offset: i64,
            max: i64,
            size: i64,
            links: Vec<Link>,
        }

        #[derive(Deserialize)]
        struct Link {
            rel: String,
            uri: Url,
        }

        let raw = RawPagination::deserialize(deserializer)?;
        
        // 从links数组中筛选出rel为"next"的链接
        let next = raw.links.into_iter()
            .find(|link| link.rel == "next")
            .map(|link| link.uri);

        Ok(Pagination {
            next,
            max: raw.max,
            offset: raw.offset,
            size: raw.size,
        })
    }
}

方式二:为单个字段指定反序列化函数

保留derive(Deserialize),通过serde属性为next字段绑定自定义反序列化函数,代码更简洁:

use serde::de::Deserializer;
use serde::{Deserialize, Deserializer as _};
use url::Url;

#[derive(Deserialize, Debug)]
pub struct Pagination {
    // 告诉serde:JSON中的"links"字段对应结构体的"next"字段,并用自定义函数解析
    #[serde(rename = "links", deserialize_with = "deserialize_next_link")]
    pub next: Option<Url>,
    pub max: i64,
    pub offset: i64,
    pub size: i64,
}

// 自定义反序列化函数:将links数组转换为Option<Url>
fn deserialize_next_link<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
where
    D: Deserializer<'de>,
{
    #[derive(Deserialize)]
    struct Link {
        rel: String,
        uri: Url,
    }

    // 先将JSON反序列化为链接数组
    let links: Vec<Link> = Deserialize::deserialize(deserializer)?;
    
    // 查找并返回next链接
    Ok(links.into_iter()
        .find(|link| link.rel == "next")
        .map(|link| link.uri))
}
测试验证

修改原有测试代码,直接断言next字段即可,新增无next链接的测试场景覆盖边界情况:

use url::Url;

use super::Pagination;

#[test]
fn test_deserialization_1_link(){
    let pagination_json = r#"{
        "offset":0,
        "max":20,
        "size":20,
        "links":[
            {
                "rel":"next",
                "uri":"https://localhost/api/v1/endpoint?offset=20"
            }
        ]
    }"#;
    let result = serde_json::from_str::<Pagination>(pagination_json).unwrap();
    assert_eq!(result.next.unwrap(), Url::parse("https://localhost/api/v1/endpoint?offset=20").unwrap())
}

#[test]
fn test_deserialization_2_link(){
    let pagination_json = r#"{
        "offset":0,
        "max":20,
        "size":20,
        "links":[
            {
                "rel":"next",
                "uri":"https://localhost/api/v1/endpoint?offset=20"
            },
            {
                "rel":"prev",
                "uri":"https://localhost/api/v1/endpoint"
            }
        ]
    }"#;
    let result = serde_json::from_str::<Pagination>(pagination_json).unwrap();
    assert_eq!(result.next.unwrap(), Url::parse("https://localhost/api/v1/endpoint?offset=20").unwrap())
}

#[test]
fn test_deserialization_no_next_link(){
    let pagination_json = r#"{
        "offset":20,
        "max":20,
        "size":20,
        "links":[
            {
                "rel":"prev",
                "uri":"https://localhost/api/v1/endpoint"
            }
        ]
    }"#;
    let result = serde_json::from_str::<Pagination>(pagination_json).unwrap();
    assert!(result.next.is_none())
}
依赖配置

确保Cargo.toml中依赖版本正确:

[package]
name = "my_project"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1.*", features = ["derive"] }
serde_json = "1.*"
url = { version = "2.*", features = ["serde"] }
jsonapi = "0.7.0"
reqwest = { version = "0.11.*", features = ["json"] }
tokio = { version = "1.*", features = ["full"] }

内容的提问来源于stack exchange,提问作者Pollastre

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 08:35:09