如何用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
相关产品推荐
相关产品推荐

