Deserialise `""` to `Option::<url::Url>::None`

I recently switched to using url::Url in my api wrapper structure instead of String. However one big problem I have is that whenever a field has "" it doesn’t give me a None, instead it throws a parse error. This makes the wrapper unusable. I made an issue for this, but are there any serde attributes I can use to temporarily circumvent this?

Erroring out when given the empty string is the correct behaviour from the url crate, so you might want to look into the #[serde(deserialize_with = "...")] attribute to add your own deserializing logic.

Maybe something like this will work?

use serde::de::{Deserialize, Deserializer, Error as _};
use std::borrow::Cow;
use url::Url;

#[derive(Debug, serde::Deserialize)]
struct Foo {
    #[serde(deserialize_with = "deserialize_url")]
    url: Option<Url>,
}

fn deserialize_url<'de, D: Deserializer<'de>>(de: D) -> Result<Option<Url>, D::Error> {
    // first, deserialize into a temporary value so we can do the empty string
    // test.
    let intermediate = <Option<Cow<'de, str>>>::deserialize(de)?;

    // now, try to parse the string as a URL, using pattern matching to handle
    // the empty string case
    match intermediate.as_deref() {
        None | Some("") => Ok(None),
        Some(non_empty_string) => Url::parse(non_empty_string)
            .map(Some)
            .map_err(D::Error::custom),
    }
}

fn main() {
    let json = r#"{ "url": "" }"#;
    let foo: Foo = serde_json::from_str(json).unwrap();

    println!("{foo:?}");
}

(playground)

2 Likes

Erroring out when given the empty string is the correct behaviour

Yup that's what I expected :frowning: I understand that it can't really be implemented in a 'direct' way.