#[Serde] Pick variant based on data

37 messages · Page 1 of 1 (latest)

frigid haven
#

I have an API here--which I can't change--where {"foo": "bar", "error": ""} is a success and {"error": "nope"} is a failure. My current implementation handles this via

#[derive(Deserialize, Debug)]
struct Response<T> {
    #[serde(flatten)]
    data: Option<T>,
    error: String
}

but this eats deserialization errors in the data part: they become simply Response { data: None, error: "" }. How do I get something more detailed out of it? It'd be nice to end up with a Result<T, String>, hence the title.

edgy kernel
prime rover
#

I think you might be best off writing a custom Deserialize impl that deserializes the error string, checks whether it is empty, then uses that to decide how to fill data

edgy kernel
#

Thinking you could use an untagged Result that contains a tagged with "" single-variant enum for data and a normal struct for error.

frigid haven
#
#[derive(Deserialize, Debug)]
#[serde(tag="error")]
enum Response2<T> {
    #[serde(rename="")] Data(T),
    #[serde(other)] ApiError{ error: String }
}

doesn't do the job--it complains that ApiError isn't unit and that error can't be a field name because it's already used for the tag, so yeah, probably gotta just deserialise twice.

edgy kernel
#

You don't have to deserialize twice. At the worst you'd need to deserialize into an intermediate type before the final type.

frigid haven
#

Because I can't use a Deserializer more than once?

frigid haven
edgy kernel
#

This is what I was thinking:

#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum ApiResult {
    Ok(ApiSuccess),
    Err(ApiError),
}

#[derive(Debug, Deserialize)]
#[serde(tag = "error", content = "data")]
pub enum ApiSuccess {
    #[serde(rename = "")]
    Data(String)
}

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct ApiError {
    error: String
}
#

Actually lemme add #[serde(deny_unknown_fields)]

frigid haven
#

There's no data in the input though

edgy kernel
#

Oh right, I replaced foo with data

frigid haven
#

There can be more than one field, too, hence why I used flatten

#

In typescript land, ApiResult<T> = (T & {error: ""}) | { error: string}

edgy kernel
#

Forgot about from, how about this:

#[derive(Debug, Deserialize)]
struct ResponseUncertain<'a, T> {
    #[serde(flatten)]
    data: T,
    error: &'a str,
}

impl<T> From<ResponseUncertain<'_, T>> for ResponseResult<T> {
    fn from(value: ResponseUncertain<T>) -> Self {
        if value.error.is_empty() {
            ResponseResult::Ok(ResponseSuccess { data: value.data })
        } else {
            ResponseResult::Err(value.error.to_string())
        }
    }
}

#[derive(Debug, Deserialize)]
#[serde(from = "ResponseUncertain<T>")]
enum ResponseResult<T> {
    Ok(ResponseSuccess<T>),
    Err(String),
}

#[derive(Debug, Deserialize)]
struct ResponseSuccess<T> {
    #[serde(flatten)]
    data: T,
}
#

I wish there was a way to use std's result, but I don't think that's possible since serde wants it externally tagged. You'd just have to convert it when necessary.

#

ah, forgot the Option

#

probably want to use TryFrom instead then

frigid haven
#

That would do the conversion but still eat the error message if what we have isn't actually a T & {error: ""}, though, wouldn't it?

edgy kernel
#

This would eat the data if the error isn't "". If you convert it to TryFrom you could have the deserialization fail if there's data and error.

#

or you could make another variant for those

frigid haven
#

I mean, like, if deserialising as T would fail, my original struct won't.

frigid haven
edgy kernel
#

That depends. If it's serde_json::Value, it'll just be an empty object and succeed.

frigid haven
#

Since I know this is JSON I guess I can do struct UncertainResponse { #[flatten] data: serde_json::Value, error: String }

edgy kernel
#

With TryFrom:

#[derive(Debug, Deserialize)]
struct ResponseUncertain<'a, T> {
    #[serde(flatten)]
    data: Option<T>,
    error: &'a str,
}

impl<T> TryFrom<ResponseUncertain<'_, T>> for ResponseResult<T> {
    type Error = &'static str;

    fn try_from(value: ResponseUncertain<'_, T>) -> Result<Self, Self::Error> {
        Ok(if value.error.is_empty() {
            ResponseResult::Ok(ResponseSuccess {
                data: value.data.ok_or("No data found")?,
            })
        } else {
            ResponseResult::Err(value.error.to_string())
        })
    }
}

#[derive(Debug, Deserialize)]
#[serde(try_from = "ResponseUncertain<'_, T>")]
enum ResponseResult<T> {
    Ok(ResponseSuccess<T>),
    Err(String),
}

#[derive(Debug)]
struct ResponseSuccess<T> {
    data: T,
}
#

Hmm, this does eat the data if T fails.

frigid haven
#

Making data be serde_json::Value and using from_value on it afterwards does preserve the error, though.

viscid pulsar
#

Even in the success case, you have the possibility of data being empty?

edgy kernel
#

The error is only dropped if it's "" so I don't think you need to worry about it disappearing.

viscid pulsar
#

Are these the three possibilities for your json:

  1. No error: { "foo": "bar", "baz": "qux", "error": ""}
  2. Error: { "error": "oh no!" }
  3. Error w/ partial response: { "error": "oh no!", "bar": "baz" }

Is { "error": "" } a possibility?

#

If not, I would design it as follows:

struct Response<T> {
    data: T,
}

struct RequestError<T> {
    data: Option<T>,
    error: String,
}

impl<T> TryFrom<ResponseUncertain<'_, T>> for Response<T> {
    type Error = RequestError<T>;
    fn try_from(value: ResponseUncertain<'_, T>) -> Result<Self, Self::Error> {
        match (value.error, value.data) {
            ("", Some(data)) => Ok(Response { data }),
            ("", None) => Err(Self::Error { data: None, error: "Received neither an error nor data" }),
            (error, maybe_data) => Err(Self::Error { data: maybe_data: error }),
        }
    }
}
#

Here, RequestError contains optional data, if you want to do something with it even in the case of an error

edgy kernel
#

Is it possible to recover that error from serde_json::from_str?

#

I think you have to downcast it, which sucks

viscid pulsar
#

Oh, interesting. I don't know how serde_json::from_str interfaces with the error type in a TryFrom impl.

edgy kernel
#

Also the error message from the API is not a serialization or rust error. It's a well-formed message that should still be deserialized.