Serde try_from fails with "missing field"

Hello

I am trying to use the try_from feature of serde but it is not behaving as I expected.
As you can see in my example, I want to convert JSON message containing a result field, indicating success or not, into a Result<T> without this result field.

use serde::Deserialize;
use serde_json as json;
use std::convert::TryFrom;

#[derive(Deserialize, Debug)]
#[serde(try_from = "Response<Device>")]
struct Device {
    model: String,
    version: String,
}

#[derive(Deserialize)]
struct Response<T> {
    result: String,
    #[serde(flatten)]
    inner: T,
}

impl TryFrom<Response<Device>> for Device {
    type Error = String;

    fn try_from(response: Response<Device>) -> Result<Device, String> {
        if response.result == "success" {
            Ok(response.inner)
        } else {
            Err(response.result)
        }
    }
}

fn main() {
    let msg = r#"{
        "result": "success",
        "model": "foobar",
        "version": "v1.0.0"
    }"#;
    let _: Device = json::from_str(msg).unwrap();

    let msg = r#"{
        "result": "error"
    }"#;
    let _: Device = json::from_str(msg).unwrap();
}

(Playground)

Well creating a Response<Device> involves setting the model field because that's how you defined a Response. You want it to be optional somehow.

I guess you are talking about the second example with "result": "error". If you run the example you'll see that the issue arise at the first example. It seems to complains about a missing result field in the Device struct. My understanding of how it works is that it deserialize the message into Response<Device> where all necessary fields are present, then do try_from to convert into a Device.

When you deserialize a Response<Device>, you start with the result field, but then you need to deserialize inner. For this you look at the Deserialize of Device, which is again a Response<Device>, but now you do not have a result field anymore, since you used that up already.

In other words, the Deserialize implementation is recursive without an exit condition.

I understand. What I am trying to do initially is to deserialize different messages into a Result<T> based on their result field: Ok if result == "success", Err otherwise. I don't want the result field in T. Is there a way to do that?

I think I'd keep things simple and go with something like

#[derive(Deserialize, Debug)]
struct Device {
    model: String,
    version: String,
}

#[derive(Deserialize, Debug)]
#[serde(tag = "result")]
#[serde(rename_all = "lowercase")]
enum Response<T> {
    Success(T),
    Error,
}

and create some helper method or implement Into/From to turn the response into a Result if necessary. You might be able to use TryFrom with a newtype (struct MyResult<T>(Result<T, String>)), but I'm not sure the end result would look any better.

1 Like