rustserdeprost

How can I deserialize a Prost enum with serde?


I'm using [prost] to generate structs from protobuf. One of those structs is quite simple:

enum Direction {
  up = 0;
  down = 1;
  sideways = 2;
}

This generates code which looks like:

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)]
#[derive(serde_derive::Deserialize)]
pub enum Direction {
    Up = 0,
    Down = 1,
    Sideways = 2,
}

There's a significant number of JSON files I have to parse into these messages. Those are tens of thousands of lines long, but when this field appears it looks like:

{ "direction": "up" }

So, in short, its deserialized format is a string, serialized is i32.

If I just run that, and try to parse JSON, I get:

thread 'tests::parse_json' panicked at 'Failed to parse: "data/my_data.json": Error("invalid type: string \"up\", expected i32", line: 132, column: 23)

That, of course, makes sense - there's no reflection to guide deserialization from "up" to 0.

Question: How can I set up serde to parse those strings into their matching integer values? I've read the serde docs thoroughly, and it appears I might need to write a custom deserializer for this, although that seems like overkill.

I've tried a few different serde attributes, like:

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)]
#[derive(serde_derive::Deserialize)]
#[serde(from = "&str")] // This line
pub enum Direction {
    Up = 0,
    Down = 1,
    Sideways = 2,
}

with this function:

impl From<&str> for Direction {
    fn from(item: &str) -> Self {
        match item {
            "up" => Self::Up,
            "down" => Self::Down,
            "sideways" => Self::Sideways,
            _ => panic!("Invalid value for Direction: {}", item),
        }
    }
}

but, despite what the serde docs tell me, that method doesn't even get called (but compilation succeeds).

I also tried a field attribute on the field which is a Direction:

#[serde(deserialize_with = \"super::super::common::Direction::from\")]

but that of course wants a different signature than just &str: the trait std::convert::From<__D> is not implemented for common::Direction

Do I just have to write a custom deserializer? Seems like a common enough use case that there would be a pattern to use.

Note: this is the opposite problem of that solved by serde_repr. I didn't see a way to put it to work here.


Solution

  • I implemented my own deserializer, thanks to the guide in this answer. There's likely a simpler or more idiomatic approach out there, so if you know one, please share!

    Serde attribute, set on the field rather than the Enum:

    config.field_attribute(
        "direction",
        "#[serde(deserialize_with = \"super::super::common::Direction::from_str\")]"
    );
    

    Deserializer:

    impl Direction {
        pub fn from_str<'de, D>(deserializer: D) -> Result<i32, D::Error>
        where
            D: Deserializer<'de>,
        {
            let s: &str = Deserialize::deserialize(deserializer)?;
    
            match s.to_lowercase().as_str() {
                "up" => Ok(Self::Tx as i32),
                "down" => Ok(Self::Down as i32),
                "sideways" => Ok(Self::Sideways as i32),
                _ => Err(de::Error::unknown_variant(s, &["up", "down", "sideways"])),
            }
        }
    }