How to deserialize to multiple possible types using reqwest & serde_json

I'm using reqwest to call an endpoint that returns two distinct json object types - either the requested item (which I deserialize to a locally-defined type), or an error. As I don't know what more specific type to deserialize to, I'm using a serde_json::Value, then branching on the existence of an identifying json key (error in the example below). Something like:

// struct for the json object returned by a successful api call
struct Checklist {
           id: i32,
           name: String
          [etc]
}

let list: serde_json::Value = self
            .client // reqwest client
            .get(url)
            .send()
            .await?
            .json()
            .await?;

if let Some(json_object) = list.as_object() {
    if json_object.contains_key("error") {
      // handle error appropriately
    } else {
        let checklist: Checklist = serde_json::from_value(list).unwrap();
        Ok(checklist)
    }
} else {
     // handle error appropriately
}
    

Naive I realise, but it got me started. I briefly played with creating en enum type containing 'Checklist' and error variants, just for the deserialization, but I couldn't get that to work. In any case it semed clumsy to me to create a type for such a transitory purpose.

What would be a better way to do this?

Actually, types are your best friend in Rust, since the sophisticated type system can be used to relieve you of much trouble. So creating lots of transitory types is very common. Only begin to re-think about these if they become a performance bottleneck.

What problem did you face? Maybe we can help with that...

3 Likes

I didn't keep the code, but it was things along the lines of:

enum GetListResponse {
      ValidList(Checkist),
      Error{error: String},
}

And then using that as the call return type. I got some deserialization error or other, then realised I didn't know enough about how to use enums with serde_json.

I also wondered if there mightn't be an alternative way, using Checklist as the api call Result type, and then some of the Result combinators to handle the case when the expected object isn't returned.

I didn't spend a lot of time on this as it's suhc a common situation when calling APIs, there must be some established idioms to learn from.

And how are your API responses shaped? From the code your wrote, it suggests that a successful reply would be of the form: { "ValidList": { .... } } and an error reply would be of the form: { "Error": { error: "<msg>" } }.

1 Like

The valid case is something like {"id": [integer], "name": "something", .. [other fields as per the Checklist struct]}. The invalid case is {"error": "description"}

Ah, well, then you have to either do the approach you have taken, or write a manual Deserialize trait. I'd say the approach you have taken is the simpler one.
Do what @Hyeonu recommended.

1 Like

Use untagged attribute.

#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
enum GetListResponse {
    Error { error: String },
    ValidList(Checklist),
}
5 Likes

Cheers that works nicely. Thanks for the link - I was working from the API docs, and hadn't come across serde.rs. Lots to read up on there.

So with calling APIs in general, is it idiomatic Rust to always try and model the response body with a type? In this case it seemed odd to me in part because I won't need this type in any other methods or functions - so strictly speaking the enum definition belongs within the function.

If all the endpoints have the same error format and it never overlaps with the success response types you can create a generic version of the enum. But that can get a little messy if you haven't worked with serde much yet.

1 Like

In this case you can even declare the type itself within the function body.

Is spamming types really idiomatic in Rust? Usually it is. I'd call it a declarative approach - you define the structure of the data you want, and the library/macro generates correct and efficient code for it. For many cases it's more convenient, less error prone and easier for readers to understand what it expects.

2 Likes

OK well thanks for the advice. I'm amused by the spamming with types notion. In this case (a learning project) the only victims will be myself & the compiler so I guess I can do my worst.