Pollastre
Pollastre

Reputation: 154

serde deserialize json array into option struct

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(())
}

The problem

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?

Edit

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())
}

Edit 2

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"]}

Upvotes: 1

Views: 116

Answers (1)

drewtato
drewtato

Reputation: 12812

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,
}

Upvotes: 3

Related Questions