Recently I ran into a bug in my code; hey, it happens. The bug was that I had a struct which could serialize into json, but could not deserialize from its own json. The struct holds a value for a mac address, which is 48-bit integer (that i store in a u64), but it is serialized using the network interface name. For example on my mac, i have a network interface named en1 with the mac address of 20:c9:d0:b0:a4:71:

(Semi-spoiler,dtolnay showed this method as a comment to my previous reddit post. I already had this written, so i decided to post and give some explanation on it anyway).

%› ifconfig en1 ether 
en1: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	ether 20:c9:d0:b0:a4:71

So, the JSON { "mac_addr": "en1" } will deserialized into the struct MacAddrExample { mac_addr: 36051161752689 }. The reason behind this kind of deserialization is that I use this to deserialize a config file, and i don’t want MAC address in the config file, i only want network interface names.

The simplified struct is basic,

 #[derive(Debug, Serialize, Deserialize)]
 struct MacAddrExample {
    #[serde(deserialize_with = "mac_to_u64")]
    mac_addr: u64
 }

So, if I deserialze { "mac_addr": "en1" } to get the sturct MacAddrExample { mac_addr: 36051161752689 }, then serialize that struct i’ll get { "mac_addr": 36051161752689 }, then deserialize that JSON, and boom, panic!

Error("invalid type: integer `36051161752689`, expected a string"

That’s a bust.

I want to be able to deserialize both … which sounds familiar. However in my previous example, i was deserializing an enum, whereas this time i’m deserializing a primitive. If you try to implement Deserialize for a primitive, you’ll get an error since serde already defines that trait for u64:

error[E0119]: conflicting implementations of trait `serde::Deserialize<'_>` for type `u64`:
  --> src/main.rs:74:1
   |
74 | impl<'de> Deserialize<'de> for u64 {
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: conflicting implementation in crate `serde`:
           - impl<'de> serde::Deserialize<'de> for u64;

And, you get an error because of Rust’s orphan rules for trait implementations.

error[E0117]: only traits defined in the current crate can be implemented for arbitrary types
  --> src/main.rs:74:1
   |
74 | impl<'de> Deserialize<'de> for u64 {
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ impl doesn't use types inside crate
   |
   = note: the impl does not reference any types defined in this crate
   = note: define and implement a trait or new type instead

Double whammy!

We need to find another way to get this job done. If you recall the definition of the the struct above, the struct uses the attribute deserialize_with which tells serde to use a function to deserialize the data, in this case, the function has the function signature of:

fn mac_to_u64<'de, D>(deserializer: D) -> Result<u64, D::Error>
    where D: Deserializer<'de>

This must take an object that is generic over Deserializer as input. But now to we make that deserialize a string an u64? It is possible to do, but we need to introduce a new enum which contains the types we want to accept as input (i.e. String and u64) and deserialize that enum using a function.

#[derive(Deserialize)]
#[serde(untagged)]
enum MacOrU64 { U64(u64), Mac(String) }
pub fn mac_or_u64<'de, D>(deserializer: D) -> Result<u64, D::Error>
    where D: Deserializer<'de>
{
    match MacOrU64::deserialize(deserializer)? {
        MacOrU64::U64(v) => { Ok(v) }
        MacOrU64::Mac(v) => {
            mac_addr_as_u64(&v).ok_or(serde::de::Error::custom("Can't parse MAC address"))
        }
    }
}

Note the #[serde(untagged)] attribute which makes the serialized JSON an anonymous object. See the serde documentation for serge’s enum representations.

The body of the function deserialize the enum U64OrMac and you just need to match on the result.

Now we just need to change the attribute of the oringal struct to use the new function:

#[derive(Debug, Serialize, Deserialize)]
struct MacAddrExample {
    #[serde(deserialize_with = "mac_or_u64")]
    mac_addr: u64
}

This deserialize pattern can be extended to be able to deserialize from several different kinds of objects.

In my sample core repo, I run through this example, and i also show how to deserialize from a hex string (i.e. “0x123”) or integer into a u16 type.

Serde’s docs are pretty good. I recommend their documentation page as well as their Github Page for JSON examples. I also recommend reading about their enum representations.

My Example code can be found in my gitlab examples repo.