Skip to content

1.0 better internal serde usage #146

@TroyKomodo

Description

@TroyKomodo

Hello!
I noticed you are in the process of a 1.0 stabilization.

While going through some of the changes I noticed a few things I wanted to raise attention of which may seem insignificant but I find quite important when writing a serde::de::Deserializer

  1. If the format we are implementing has a direct representation for the type they are requesting then we forward the call todeserialize_any .

    For example, if we can represent all integer types i8 i16 i32 i64 and they request an i8 and we have an i32 we should return the i32 do not try and coerce the type.

  2. If we cannot represent the type try coerce the value we have into the value they expect, and if we cant do that forward to deserialize_any

    For example if we only have f64 as the only number type and they request an i32 we try convert our f64 into an i32 or if we don’t have a number type and numbers are typically represented as strings then try parse the number from a string.

  3. The first two rules imply this, however for the case of being explicit. Avoid returning errors from the deserializer at all costs. Always forward to the visitor. Of course if the serialized content is invalid then that is an acceptable error, but never return an error for mismatched type vs expectations. (ie, they called deserialize_string and we have map just call visit_map instead of throwing an error.

-- Actual Examples from the code

serde_qs/src/de.rs

Lines 473 to 490 in f24f916

fn deserialize_tuple<V>(
self,
len: usize,
visitor: V,
) -> std::result::Result<V::Value, Self::Error>
where
V: de::Visitor<'de>,
{
if let Some(seq_len) = self.seq_length() {
if seq_len != len {
return Err(Error::custom(
format!("expected a tuple of length {len}, got length {seq_len}"),
&self.0,
));
}
}
self.deserialize_seq(visitor)
}

During deserialize_tuple we should not do a bounds check since this isnt a "real error" (such as an invalid state or deserialization from the underlying text buffer), this is just a check that is done in code. We should just forward what we have to the visitor and then let the visitor return the error for us. Who knows, maybe the visitor is able to handle this less then full length tuple situation.

serde_qs/src/de.rs

Lines 577 to 588 in f24f916

fn deserialize_unit<V>(self, visitor: V) -> Result<V::Value>
where
V: de::Visitor<'de>,
{
if matches!(self.0, ParsedValue::NoValue | ParsedValue::Null)
|| matches!(self.0, ParsedValue::String(ref s) if s.is_empty())
{
visitor.visit_unit()
} else {
Err(Error::custom("expected unit".to_owned(), &self.0))
}
}

During deserialize_unit like in the tuple function always forward to deserialize_any (rule 1) if we dont have the type the visitor expects and we have a way of representing it in our state.

In some places this rule is upheld like such

serde_qs/src/de.rs

Lines 613 to 630 in f24f916

fn deserialize_enum<V>(
self,
_name: &'static str,
_variants: &'static [&'static str],
visitor: V,
) -> Result<V::Value>
where
V: de::Visitor<'de>,
{
match self.0 {
ParsedValue::Map(mut parsed) => visitor.visit_enum(MapDeserializer {
parsed: &mut parsed,
field_order: None,
popped_value: None,
}),
ParsedValue::String(s) => visitor.visit_enum(StringParsingDeserializer::new(s)?),
_ => self.deserialize_any(visitor),
}

I propose this should always be the case since from the Deserializer's perspective the deserialize_x functions are mealy "hints" as to what the visitor would like us to return, if we cannot convert to that type we should just give the visitor what we have and hope they can figure it out, in most cases they likely wont be able to and they will return an error, but we shouldn't impose hard limits on visitor implementations.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions