I am retrieving data from an API that uses pagination. I want to deserialize the pagination object into my own rust struct. These objects look like this:
{
"offset":0
, "max":20
, "size":20
, "links":[
{
"rel":"next"
, "uri":"https://.../api/v1/endpoint?offset=20"
}
, {
"rel":"prev"
, "uri":"https://.../api/v1/endpoint"
}
]
}
where offset
, max
, and size
are always given and links
has either or both of next
or prev
links.
I then use the following structs to parse the above json into a Pagination
struct:
use serde::*;
use serde_json::Result;
#[derive(Deserialize, Debug)]
#[serde(tag = "rel")]
enum PaginationRef {
#[serde(alias = "next")]
Next { uri: Url },
#[serde(alias = "prev")]
Prev { uri: Url },
}
// I know the list has at most 2 links
#[derive(Deserialize, Debug)]
struct PaginationLinks(
#[serde(default)] Option<PaginationRef>,
#[serde(default)] Option<PaginationRef>,
);
#[derive(Deserialize, Debug)]
pub struct Pagination {
links: PaginationLinks,
max: i64,
offset: i64,
size: i64,
}
fn main() -> Result<()> {
let data = r#"
{
"offset":0
, "max":20
, "size":20
, "links":[
{
"rel":"next"
, "uri":"https://.../api/v1/endpoint?offset=20"
}
, {
"rel":"prev"
, "uri":"https://.../api/v1/endpoint"
}
]
}
"#;
let v: Pagination = serde_json::from_str(data)?;
println!("{:#?}", v);
Ok(())
}
I only care about the next
link from the json string. I want to simplify the Pagination
struct into the following:
#[derive(Deserialize, Debug)]
pub struct Pagination{
next: Option<Url>,
max: i64,
offset: i64,
size: i64,
}
I have tried to use field attributes to get the result I want, but I don't think they work for this problem. I am guessing I need to write a custom deserializer to turn "links":[...]
into an Option<Url>
. How can I achieve this?
Here are the tests I implemented. Both of them pass:
pagination.rs
// Implemented to test correct parsing (insufficient)
impl Pagination {
pub fn get_next_page_link(&self) -> Option<&Url> {
if let Some(page) = &self.links.0 {
let result = match page {
PaginationRef::Next{uri} => Some(uri),
_ => None,
};
return result;
}
return None;
}
}
pagination/tests.rs
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.get_next_page_link().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.get_next_page_link().unwrap(), &Url::parse("https://localhost/api/v1/endpoint?offset=20").unwrap())
}
I have already accepted @drewtato's answer, but here's my cargo.toml file:
[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
jsonapi="0.7.0"
serde={"version" = "1.*", "features" = ["derive"]}
serde_json="1.*"
url = { version = "2.*", features = ["serde"] }
reqwest={"version" = "0.11.*", "features" = ["json"]}
tokio={"version" = "1.*", "features" = ["full"]}
You can use your existing types to make a deserialize function. I've removed uri
from Prev
since that field is never read. This is fine since Serde ignores extra fields by default.
#[derive(Deserialize, Debug)]
struct PaginationLinks(
#[serde(default)] Option<PaginationRef>,
#[serde(default)] Option<PaginationRef>,
);
#[derive(Deserialize, Debug)]
#[serde(tag = "rel", rename_all = "snake_case")]
enum PaginationRef {
Next { uri: Url },
Prev {},
}
fn links<'de, D>(deserializer: D) -> Result<Option<Url>, D::Error>
where
D: Deserializer<'de>,
{
let links = PaginationLinks::deserialize(deserializer)?;
let item = [links.0, links.1]
.into_iter()
.flatten()
.find_map(|opt_pr| match opt_pr {
PaginationRef::Next { uri } => Some(uri),
_ => None,
});
Ok(item)
}
And you can use that in your simplified Pagination
struct.
#[derive(Deserialize, Debug)]
pub struct Pagination {
#[serde(rename = "links", deserialize_with = "links")]
next: Option<Url>,
max: i64,
offset: i64,
size: i64,
}