Serde error API unpleasantries


#1

First off: Serde is awesome at what it does and even generally how it achieves that.

However, I find myself in the undesirable situation where I have to manually implement Serde’s Serialize, Deserialize and Visitor traits a lot as the underlying types, and the more I have to do it, the more I find myself disliking the trait-heavy API.

Concrete example:

         // the OrderedFloat type comes from the `ordered_float` crate:

        #[derive(Copy, Clone, Debug, Hash)]
        pub struct Double { primitive: OrderedFloat<f64> }

        struct DoubleVisitor;

        impl<'de> Visitor<'de> for DoubleVisitor {
            type Value = Double;

            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("struct Double")
            }

            fn visit_str<E>(self, string: &str) -> Result<Self::Value, E>
                where E: de::Error
            {
                use serde::de::Error;
                match string.parse::<f64>() {
                    Ok(prim) => Ok(Double { primitive: OrderedFloat(prim) }),
                    Err(parse_float_err) => {
                        // ...
                    },
                }
            }
        }

And for the life of my I can’t figure out what the Err variant of visit_str should contain to make rustc shut up.
I initially thought that it should contain a value of some type that implements de::Error, and that’s also what Serde’s error handling page suggests.

But that’s just a bust as rustc just starts yapping, and the API is not helpful in the slightest in figuring out what is supposed to go there instead. If not some type that implements de::Error, then what is it supposed to be?

I have a couple of questions:

  1. Anyone any ideas on what visit_str can reasonably return without having to write a book worth of boilerplate?
  2. Anyone else feeling this frustration? Isn’t there any ergonomically superior API possible than this for de/serialization with similar performance characteristics?
  3. Why is this API so unergonomic? Where regular rust feels like a nice breeze, this feels like having to wrestle my way through thick syrup, all the while having to fight to keep away from the alligators and snakes that are compiler errors. I have that problem in general with traits in Rust though as they feel like 3rd-rate citizens to me in the ergonomics department (they’re kind-of-like types, except they’re not, and you can’t pattern match on them which is a huge downside, not to mention the trait-bound-as-return-type-weirdness witnessed above), so perhaps it’s “just” that Serde makes such heave use of it that compounds the problem.
  4. Is it perhaps, as a social measure, time to actively encourage developers to make the pub types in all their crates provide serialization support? This would be done by deriving or implementing Deserialize and Serialize. If that were the case, the need for manual implementations would basically be obviated as pretty much any new crate could just derive Deserialize and Serialize.

#2
pub struct Double { primitive: OrderedFloat<f64> };

impl<'de> Deserialize<'de> for Double {
    fn deserialize<D>(deserializer: D) -> Result<Double, D::Error>
    where
        D: Deserializer<'de>
    {
        let value = f64::deserialize(deserializer)?;
        Ok(Double { primitive: OrderedFloat(value) })
    }
}

In your original example, you’d do something like E::invalid_value(Unexpected::Str(string), &self) to construct an error.


#3

The function you’re implementing must return an arbitrary E of the caller’s choice, not just some E of the callee’s choice. The only knowledge that the callee is provided about E is that it supports the de::Error interface, which means the only (reasonable) way the callee can create an E is through one of the static methods of that trait.


#4

Is it perhaps, as a social measure, time to actively encourage developers to make the pub types in all their crates provide serialization support?

Version 0.5 of ordered-float comes with Serde support if you enable features = ["serde"].


#5

Why is this API so unergonomic? Where regular rust feels like a nice breeze, this feels like having to wrestle my way through thick syrup, all the while having to fight to keep away from the alligators and snakes that are compiler errors. […] perhaps it’s “just” that Serde makes such heave use of it that compounds the problem.

I can try to justify why the error trait mechanism is solving a hard problem.

Serde is primarily a framework for writing data format libraries. Something like serde_json or serde_yaml or bincode would be 100% possible to write without Serde, but Serde gives them a way to leverage common traits and share some code (because imagine if every data format used its own individual variations of Serialize and Deserialize and special snowflake syntax for attributes for deriving them).

But for all the shared code, each format still gets to define its own error type. So there is serde_json::Error which is distinct from bincode::Error. This is because in general every format has different reasons for why things can fail to serialize or deserialize. For example JSON can fail to deserialize a map in which the keys are not strings, because JSON syntax only allows string keys. Bincode allows maps with keys of any type, so this possibility is not included in Bincode’s error type. Any two data format error types will have lots of differences like this.

You are implementing the Deserialize trait which defines behavior that needs to work for any data format. So if the D: Deserializer type parameter happens to be serde_json, your function needs to return serde_json's error type. But if D happens to be bincode, your function needs to return bincode's error type.

You may have seen this already but the serde::de::Error trait is a menu of constructors that give all the ways a Deserialize impl is allowed to fail. These constructors give the Deserialize impl a way to construct the right data format library’s error type, whatever type that is.


#6

It’s a good while further, and I have had a chance to play around (and bang my head against) object safety in the context of trait definitions.
Would it be fair to say that serde::de::Error is not object-safe i.e. rustc would error out if you tried to create a Box<serde::de::Error>? If it isn’t object-safe, then I think I understand it now.