Writing custom serde deserializers, the easy way

- rust serde deserialization serialization

Serde is a great library for serializing/deserializing in Rust. It allows you to directly convert rust data structures using a few attributes. Most of the time, It Just Works ™ and when it doesn’t, you can write your own serializers or deserializers!

Problem at hand: There exists a type which can be parsed from a string but serde has no built in support for it. For the purposes of an example, let’s say the type is std::net::IpAddr and forget that serde has support for it already. This avoids us the distraction of creating a new type and parser.

Before jumping to the “easy” way, let’s first follow the official documentation and try writing a deserializer. for the following struct:

struct Config {
    host: IpAddr,
    port: u16,
}

To use the built-in parser, we need to build a visitor that convers a string to our type.

struct AddrVisitor;

impl<'de> de::Visitor<'de> for AddrVisitor {
    type Value = std::net::IpAddr;

    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
        formatter.write_str("a dotted-decimal IPv4 or RFC5952 IPv6 address")
    }

    fn visit_str<E: de::Error>(self, s: &str) -> Result<Self::Value, E> {
        s.parse().map_err(de::Error::custom)
    }
}

fn parse_addr<'de, D>(deserializer: D) -> Result<IpAddr, D::Error>
where
    D: Deserializer<'de>
{
    deserializer.deserialize_str(AddrVisitor)
}

Try it on the playpen

It’s… quite a handful. A visitor may make sense if you want to support different types, but a lot of the times you really just want to convert between one of the base types (string / integer) to our domain type, which makes the flexibility of a visitor quite an overkill.

We can easily get rid of all this boilerplate with the following pattern:

fn parse_addr<'de, D>(deserializer: D) -> Result<IpAddr, D::Error>
    where D: Deserializer<'de>
{
    let addr_str = String::deserialize(deserializer)?; // <-- this let's us skip the visitor!
    addr_str.parse().map_err(de::Error::custom)
}

Try it on the playpen

We went from 20 lines of boilerplate to 5 lines! We leverage serde’s existing string deserializer to first get the string, which can be readily parsed! You can analogously use any of the base types serde supports and convert to the relevant domain type.

Reading the serde docs didn’t make it obvious to me, so I hope this will be useful to somebody and save some boilerplate.