Is it possible to directly parse an inner JSON using serde?

Take for example this GoogleBooks API response

{
  "kind": "books#volumes",
  "totalItems": 10,
  "items": [
    {
      "kind": "books#volume",
      "id": "id",
      "etag": "tag",
      "selfLink": "link.to.somewhere",
      "volumeInfo": {
        "title": "some title",
        "authors": [
          "author 1",
          "author 2"
        ],
        "publisher": "some publisher",
        "...": "..."
      }
    },
    {
      "...": "...",
      "volumeInfo": {}
    },
    {
      "...": "...",
      "volumeInfo": {}
    }
  ]
}

I am only interested in values of "volumeInfo" key but because it's nested inside "items", I need to deserialize outer levels to get to it.

Fortunately serde makes it very easy:

#[derive(Debug, Deserialize)]
struct Items {
    items: Vec<VolumeInfo>,
}

#[derive(Debug, Deserialize)]
struct VolumeInfo {
    #[serde(rename = "volumeInfo", deserialize_with = "deserialize")]
    volume_info: Book,
}

But problem get compounded if there's a key that depends on the arguments you provided for the call, for example this OpenLibrary API response

{
  "ISBN:SomeISBN": {
    "url": "link.to.somewhere",
    "key": "...",
    "title": "...",
    "subtitle": "...",
    "authors": [ "..." ],
    ...
    }
}

Here the key "ISBN:SomeISBN" depends on the the call args so I am having to first deserialize this response into just serde_json::Value

let mut response: serde_json::Value = reqwest::get(req)...?;

and then index into response and parse it again

struct ISBNResponse(#[serde(deserialize_with = "deserialize")] Book);

let ISBNResponse(book) = serde_json::from_value(response[isbn_key].take())...?

I am not sure if parsing into serde_json::Value and parsing again is more expensive than parsing into a struct like I did for Google Books but either way I think parsing the relevant inner struct directly could be faster than either.

Looking around I found
#[serde(flatten)] and
#[serde(tag = "...", content = "...")] but I don't think that's exactly what I am looking for.

A container with arbitrary dynamic keys is not modelled well by a struct. Shouldn't you be deserializing into a HashMap<String, Book> instead?

If I understand it correctly, wouldn't that requires a Deserialize implementation for Book like so

impl<'de> Deserialize<'de> for Book

but because those API respond with very different structure for essentially the same data, I rely on deserialize_with = "function_name" attribute so both of those responses can be parsed into singular struct Book.

I don't know if there is a way to use that attribute or that sort of functionality outside struct.

I could also use tuple struct with single Book inside and implement Deserialize for it but I would like to preserve &str -> Result<Book> function signature if possible.

I don't understand what you are asking here. Yes, that's the signature of the Deserialize trait. Is there anything wrong with it?

What kind of responses? What are you referring to by "both"?

If you have a function with the signature &str -> Result<NewtypeAroundBook, E> then it's trivially turned into a &str -> Result<Book, E> by mapping |x| x.0 over the returned Result.

You have many possibilities to deserialize into a HashMap<String, Book> even though Book does not implement Deserialize.

  1. You can use a deserialize_with function. You can either use a wrapper struct or create the Deserializer and call the function directly. The deserialize_with function needs to be aware of the HashMap in that case.
#[derive(Deserialize)]
struct Wrapper (
    #[serde(deserialize_with = "...")]
    HashMap<String, Book>
);
  1. You can use newtypes and apply the deserialize_with function in the newtype. You can then deserialize into a HashMap<String, BookWrapper>.
#[derive(Deserialize)]
struct BookWrapper (
    #[serde(deserialize_with = "...")]
    Book
);
  1. You can avoid complication the deserialize_with function and also avoid newtypes by directly applying the custom deserialization logic to the nested type in the HashMap. serde does not support this directly, but it is quite simple with extra crates.

The 3rd option looks really promising.
Thank you for the advice!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.