[serde] deserialize empty string as Option::None

I have a web API (not under my control), that returns either an empty string or a JSON object.
For the JSON object, I do have an appropriate Response struct that derives serde::Deserialize.
However, when I try to deserialize the response from the API as Option<Response> with serde_json, I get

   Compiling playground v0.0.1 (/playground)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.85s
     Running `target/debug/playground`
thread 'main' panicked at src/main.rs:43:59:
fuck: Error("EOF while parsing a value", line: 1, column: 0)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

iff the response was the empty string, since serde expects it to be "null" and not "".
I already read this, but it does not fit my case, since I don't want to deserialize a String, but a Response if it is non-empty.
How can I get Option::Some(Response) if the API returns a JSON object and Option::None if the response is empty?

PS: I currently work around this issue like so:

    async fn copy_scene(
        &self,
        area_id: u32,
        scene_id: u32,
    ) -> Result<Option<existing::Scene>, Error> {
        match self
            .post_json(
                &format!("{BASE_URL}/{area_id}/scenes/{scene_id}/copy"),
                Empty,
            )
            .await
        {
            Ok(scene) => Ok(Some(scene)),
            Err(error) => {
                if let Error::Serde(_) = error {
                    Ok(None)
                } else {
                    Err(error)
                }
            }
        }
    }

But I find this rather a dirty hack.

You can do this:

use serde::de::{Deserializer, Error};
use serde::Deserialize;

fn response_or_empty_string<'de, D>(d: D) -> Result<Option<Response>, D::Error>
where
    D: Deserializer<'de>,
{
    #[derive(Deserialize)]
    #[serde(untagged)]
    enum MyHelper<'a> {
        S(&'a str),
        R(Response),
    }

    match MyHelper::deserialize(d) {
        Ok(MyHelper::R(r)) => Ok(Some(r)),
        Ok(MyHelper::S(s)) if s.is_empty() => Ok(None),
        Ok(MyHelper::S(_)) => Err(D::Error::custom("only empty strings may be provided")),
        Err(err) => Err(err),
    }
}

#[derive(Deserialize)]
struct HoldsResponse {
    #[serde(deserialize_with = "response_or_empty_string")]
    resp: Option<Response>,
}
1 Like

Reposted to address OP:

You can use serde_with for this. It has a helper macro to deserialize an empty string as None: serde_with::rust::string_empty_as_none - Rust

1 Like

@alice Thank you. Is there a way to make this generic (I think not, since &'a str also implements Deserialize resulting in untagged to no longer work)? I have other structs with the same issue, not just Response.

@firebits.io Thanks. But string_empty_as_none appears to be a field attribute. How would I use that on a raw Option<T> that is not contained within a struct?

Sorry, I had not read the whole post and got mislead by the title.

I think you have conflicting requirements. There's no way to instruct the deserializer to return None rather than a deserialization error when malformed JSON is provided to it.

As long as the generic type does not deserialize from a JSON string (i.e. if it deserializes from a JSON object, array, number, null or bool instead), then there won't be any problems with making MyHelper generic over some T that implements Deserialize.

I fixed my issue now by just preprocessing an empty string, which currently fits my needs and is imho not that ugly of a hack:

pub fn parse_json<T>(json: &str) -> Result<T, serde_json::Error>
where
    T: DeserializeOwned,
{
    serde_json::from_str(fix_empty_json_str(json))
}

const fn fix_empty_json_str(json: &str) -> &str {
    if json.is_empty() {
        "null"
    } else {
        json
    }
}
1 Like

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.