Use derived `Deserialize` impl in custom `Deserialize`

Hi All,

I'm have some JSON that has some pairs like: "key": "HASS-E,HASS-S". I'd like to deserialize this into a struct Hass(Vec<HassType>); where the comma separated values get deserialized into HassTypes and placed in the Vec. How can I do this? This is my attempt:

#[derive(Debug, Serialize)]
struct Hass(Vec<HassType>);

#[derive(Debug, Serialize, Deserialize)]
enum HassType {
    #[serde(rename = "HASS-A")]
    A,
    #[serde(rename = "HASS-E")]
    E,
    #[serde(rename = "HASS-H")]
    H,
    #[serde(rename = "HASS-S")]
    S,
}

impl<'de> Deserialize<'de> for Hass {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        struct StrVisitor;
        impl Visitor<'_> for StrVisitor {
            type Value = Vec<HassType>;

            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                write!(formatter, "expected a &str")
            }

            fn visit_str<E>(self, text: &str) -> Result<Self::Value, E>
            where
                E: de::Error,
            {
                let mut quals = vec![];
                for chunk in text.split(',') {
                    match serde_json::from_str(chunk) {
                        Ok(qual) => quals.push(qual),
                        Err(e) => {
                            println!("{e}");
                            return Err(de::Error::unknown_variant(
                                chunk,
                                &["HASS-A", "HASS-E", "HASS-H", "HASS-S"],
                            ));
                        }
                    }
                }
                Ok(quals)
            }
        }

        let quals = deserializer.deserialize_str(StrVisitor)?;
        Ok(Hass(quals))
    }
}

However, this code produces an error because "HASS-*" is not valid JSON. Is there something like "deserialize" I can call to go from a string to a T?

Specifically, once I have access to the input string using visit_str, how do I deserialize the splits into HassTypes?

you don't need to manually implement Deserialize for such simple cases. you may want to use serde attributes instead, for example, check documents for #[serde(transparent)], #[serde(from = "Foo")], or #[serde(deserialize_with = "foo)]

but anyway, here I list some of the easy to spot errors in your implementation:

first of all, because your data is in map form, you should not call deserializer.deserialize_str(), instead, you should call deserializer.deserialize_map() (or alternatively deserialzier.deserialize_struct()). the deserialization should be written according to the schema of the serialized data, not the struct definition.

then, you should use Visitor::visit_map() accordingly (you may choose to validate the key or simply skip the key if you want).

finally, serde_json::from_str() expects data in the form of json string syntax, i.e. quoted (with potential escape sequences), but your splits are not quoted (identifiers instead of string). so you have to either parse HassType yourself and ditch the derived Deserialize implementation, or format the string as valid json strings then deserialize.

something like this should work (no error handling):

impl<'de> Deserialize<'de> for Hass {
	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
	where
		D: serde::Deserializer<'de>,
	{
		struct StrVisitor;
		impl<'de> Visitor<'de> for StrVisitor {
			type Value = Vec<HassType>;

			fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
				write!(
					formatter,
					"map with `key` and comma separated string value `HASS-A,HASS-E`"
				)
			}

			fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
			where
				A: de::MapAccess<'de>,
			{
				let (key, value): (String, String) = map.next_entry()?.unwrap();
				assert!(key == "key");
				Ok(value
					.split(',')
					.map(|s| serde_json::from_value(serde_json::Value::String(s.into())).unwrap())
					.collect())
			}
		}

		let quals = deserializer.deserialize_map(StrVisitor)?;
		Ok(Hass(quals))
	}
}

Thank you for the response!

I miscommunicated the format of the data, not all keys are of that form (just some have the value as a comma separated list).

Using the from attribute, I got it to work like this:

impl TryFrom<&str> for HassType {
    type Error = &'static str;

    fn try_from(s: &str) -> Result<Self, Self::Error> {
        match s {
            "HASS-A" => Ok(HassType::A),
            "HASS-E" => Ok(HassType::E),
            "HASS-H" => Ok(HassType::H),
            "HASS-S" => Ok(HassType::S),
            _ => Err("only valid HassType's are HASS-{A,E,H,S}"),
        }
    }
}

impl<'de> Deserialize<'de> for Hass {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        struct StrVisitor;
        impl Visitor<'_> for StrVisitor {
            type Value = Hass;

            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                write!(formatter, "expected a &str")
            }

            fn visit_str<E>(self, text: &str) -> Result<Self::Value, E>
            where
                E: de::Error,
            {
                Ok(Hass(
                    text.split(',')
                        .map(HassType::try_from)
                        .collect::<Result<Vec<_>, _>>()
                        .map_err(|e| de::Error::custom(e))?,
                ))
            }
        }

        deserializer.deserialize_str(StrVisitor)
    }
}

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.