Error handling & parameter validation in serde

In an effort to avoid some code duplication, I'm trying to move validation closer to the parser. My goal is relatively simple: Allow a string or a number, but if a number is given, only allow values 1-32.

It turns out post-process validation has been been on serde wish-lists for a while, and there's an open issue for it.

The actual issue post contains an example for how to parameter validation can be accomplished using deserialize_with. This works fine, apart that the specific error message gets "lost". I assume it's because the serde just iterates over the enum variants and tries to find a match, and it considers any error it encounters along the way to just mean "doesn't match this variant".

Is there a way to solve this? I.e. support both string and integer values, validate the integer, and return a specific error out-of-bounds error if the integer is out of bounds, and have that error reported back to the user?

Playground

You could create an enum with 32 variants and use serde_repr.

Nvm., this is ill-advised. I was just trying to think in ways to avoid writing a custom deserializer, but that's not the way to solve this specific problem.

1 Like

Just write a custom deserialize impl: Playground


impl<'de> Deserialize<'de> for Channel {
    fn deserialize<D>(de: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>
    {
        de.deserialize_any(ChannelVisitor)
    }
}

struct ChannelVisitor;

impl<'de> Visitor<'de> for ChannelVisitor {
    type Value = Channel;
    
    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("name (string) or number between 1..32")
    }
    
    fn visit_str<E: DeError>(self, value: &str) -> Result<Self::Value, E> {
        Ok(Channel::Name(value.to_owned()))
    }
    
    fn visit_string<E: DeError>(self, value: String) -> Result<Self::Value, E> {
        Ok(Channel::Name(value))
    }
    
    fn visit_i64<E: DeError>(self, value: i64) -> Result<Self::Value, E> {
        if (1..=32).contains(&value) {
            Ok(Channel::Num(value as u8))
        } else {
            Err(E::custom("channel number must be in 1..=32"))
        }
    }
    
    fn visit_u64<E: DeError>(self, value: u64) -> Result<Self::Value, E> {
        if (1..=32).contains(&value) {
            Ok(Channel::Num(value as u8))
        } else {
            Err(E::custom("channel number must be in 1..=32"))
        }
    }
}
3 Likes

To be fair, that's sort of what I was looking for -- but @paramagnetic's reply showed that it was far simpler than I had imagined, so I'll go for that.

1 Like

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.