json
json copied to clipboard
Deserialization `from_value` + `(Try)From` works weird
I write a deserialization using the from or try_from attribute because it saves a lot of code lines compared to manual implementation. Often deserialization comes from strings. Therefore, I implement the (Try)From<&str> trait for my type, since it is more general and doesn't require any allocation. Here is an simplified example:
#[derive(Debug, Deserialize)]
#[serde(try_from = "&str")]
struct Type;
impl<'a> TryFrom<&'a str> for Type {
type Error = &'a str;
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
match value {
"a" => Ok(Self),
_ => Err(value),
}
}
}
But when I want to test deserialization, for example in unit tests using json! macro, it doesn't work. The serde_json::from_value function gives an error, although other methods work fine:
let _: Type = serde_json::from_str("\"a\"").unwrap(); // Works fine
let _ = Type::deserialize(&serde_json::json!("a")).unwrap(); // Works fine
let _: Type = serde_json::from_value(serde_json::json!("a")).unwrap(); // Error
This behavior is highly unobvious and confusing. It's not clear why in one case everything is fine, and in the other it's not. Although, if you write the implementation manually, it also works fine:
#[derive(Debug)]
struct Type;
impl<'a> TryFrom<&'a str> for Type { /***/ }
impl<'d> Deserialize<'d> for Type {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'d>,
{
struct Helper;
impl de::Visitor<'_> for Helper {
type Value = Type;
fn expecting(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(f, "error")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Type::try_from(s).map_err(E::custom)
}
}
deserializer.deserialize_str(Helper)
}
}
Is it possible to fix this behavior?
Deserializing into a &str requires that the str can be borrowed from somewhere. This is only possible in some cases. Usually, that works if you deserialize from a slice or a &str, but not from readers or value types. You see that this line fails to compile:
let s: &str = serde_json::from_value(serde_json::json!("foo")).unwrap();
from_value requires that the type is DeserializeOwned which &str is not. DeserializeOwned basically means that it does not borrow from the input, which &str has to.
Your Type does not borrow itself, but requires that an intermediate value to borrow. The same limitations will apply to it too. In general the derive macro cannot inspect the TryFrom implementation and can thus also not warn you about this problem.
You can use other derives which prevent you from doing this specific mistake. With DeserializeFromStr you implement FromStr instead of TryFrom<&str>. Since you do not keep the reference this should not make a difference. You use DeserializeFromStr instead of Deserialize in the derive. DeserializeFromStr uses visit_str internally.
Deserializing from a borrowed string is also a bit of a footgun with json in general. When a string in a json file has escaped characters the deserializer implementation will use a temporary buffer to unescape the characters. This buffer is cleared between string parsing instances and does not share the lifetime of the input string.
see : src/de.rs:1526
Try implementing TryFrom<Cow<'a, str>. This will allow you to abstract over this case.