How can I support an unknown or other value for a Serde enum?
Asked Answered
A

5

16

I have a JSON API that returns an object that looks like this:

{
  "PrivatePort": 2222,
  "PublicPort": 3333,
  "Type": "tcp"
}

To capture this, I have an enum and a struct:

#[derive(Eq, PartialEq, Deserialize, Serialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum PortType {
    Sctp,
    Tcp,
    Udp,
}

#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct PortMapping {
    pub private_port: u16,
    pub public_port: u16,
    #[serde(rename = "Type")]
    pub port_type: PortType,
}

Right now, this API only supports the three protocols listed in PortType, but let's assume that support for DCCP is added in the future. I do not want clients of the API to start failing simply because of an unknown string in a configuration option they might not be looking at.

To address this, I've added an Unknown variant with a String to represent the value:

#[derive(Eq, PartialEq, Deserialize, Serialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum PortType {
    Sctp,
    Tcp,
    Udp,
    Unknown(String),
}

The goal here is to end up with the slightly-inconvenient PortType::Unknown("dccp") value when an unknown value is passed in. Of course, this does not do what I would like out-of-box -- passing the unknown "dccp" value will result in:

Error("unknown variant `dccp`, expected one of `sctp`, `tcp`, `udp`, `unknown`", line: 1, column: 55)

Is there a Serde configuration for doing what I want or should I resort to manually writing Deserialize and Serialize implementations for PortType?

Anti answered 12/8, 2019 at 23:47 Comment(0)
G
9

You can now use the #[serde(untagged)] attribute on the variant level, to capture unknown variants.

#[derive(Eq, PartialEq, Deserialize, Serialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum PortType {
    Sctp,
    Tcp,
    Udp,
    #[serde(untagged)]
    Unknown(String),
}

Not to be confused with the untagged attribute on the enum level.

I can't find any documentations for this attribute though.

Thanks https://github.com/serde-rs/serde/issues/912#issuecomment-1868785603

Gesualdo answered 27/12, 2023 at 20:17 Comment(0)
M
12

Try use serde-enum-str

#[derive(serde_enum_str::Deserialize_enum_str, serde_enum_str::Serialize_enum_str)]
#[serde(rename_all = "snake_case")]
pub enum PortType {
    Sctp,
    Tcp,
    Udp,
    #[serde(other)]
    Unknown(String),
}
Modulate answered 10/7, 2021 at 16:14 Comment(1)
This requires some more visibility! It solves the issue of "extensible" string enum in both directions - and also allows code programmatically to use "to_string()" and "parse()" to go between string and enum (rather than forcing the use of serde for this serialization).Shanney
G
9

You can now use the #[serde(untagged)] attribute on the variant level, to capture unknown variants.

#[derive(Eq, PartialEq, Deserialize, Serialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum PortType {
    Sctp,
    Tcp,
    Udp,
    #[serde(untagged)]
    Unknown(String),
}

Not to be confused with the untagged attribute on the enum level.

I can't find any documentations for this attribute though.

Thanks https://github.com/serde-rs/serde/issues/912#issuecomment-1868785603

Gesualdo answered 27/12, 2023 at 20:17 Comment(0)
M
7

There's an issue for this, though it's been open for 3 years with no full resolution so far. Serde #912.

What seems to be currently implemented (though undocumented) at the time of this post is #[serde(other)]. It can only be applied to unit enum fields, which limits its usefulness:

#[derive(Deserialize, PartialEq)]
#[serde(tag = "tag")]
enum Target {
   A(()),
   B(()),
   #[serde(other)]
   Others
}

fn main() {
    assert_eq!(Target::Others, from_str::<Target>(r#"{ "tag": "blablah" }"#).unwrap());
}

Other than that, the only other method as of this writing is writing your own Deserialize implementation.

Monitor answered 20/9, 2020 at 2:41 Comment(0)
B
6

I do it with serde(from="String")

#[derive(Eq, PartialEq, Deserialize, Serialize, Debug)]
#[serde(rename_all = "snake_case", from="String")]
pub enum PortType {
    Sctp,
    Tcp,
    Udp,
    Unknown(String),
}

impl From<String> for PortType {
    fn from(s: String)->Self {
        use PortType::*;

        return match s.as_str() {
            "sctp" => Sctp,
            "tcp" => Tcp,
            "udp" => Udp,
            _ => Unknown(s)
        }
    }
}

#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct PortMapping {
    pub private_port: u16,
    pub public_port: u16,
    #[serde(rename = "Type")]
    pub port_type: PortType,
}
Bombacaceous answered 12/12, 2020 at 15:51 Comment(1)
This is a neat trick, but note that it only works for "simple" enum types, i.e. those that don't have additional values.Vagina
E
3

Simple case should be fine with this:

use serde::de::Visitor;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::from_str;

#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct PortMapping {
    pub private_port: u16,
    pub public_port: u16,
    #[serde(rename = "Type")]
    pub port_type: PortType,
}

#[derive(Clone, Eq, PartialEq, Serialize, Debug)]
pub enum PortType {
    Sctp,
    Tcp,
    Udp,
    Unknown(String),
}

const PORT_TYPE: &'static [(&'static str, PortType)] = &[
    ("sctp", PortType::Sctp),
    ("tcp", PortType::Tcp),
    ("udp", PortType::Udp),
];

impl From<String> for PortType {
    fn from(variant: String) -> Self {
        PORT_TYPE
            .iter()
            .find(|(id, _)| *id == &*variant)
            .map(|(_, port_type)| port_type.clone())
            .unwrap_or(PortType::Unknown(variant))
    }
}

impl<'a> From<&'a str> for PortType {
    fn from(variant: &'a str) -> Self {
        PORT_TYPE
            .iter()
            .find(|(id, _)| *id == &*variant)
            .map(|(_, port_type)| port_type.clone())
            .unwrap_or_else(|| PortType::Unknown(variant.to_string()))
    }
}

impl<'de> Deserialize<'de> for PortType {
    fn deserialize<D>(de: D) -> Result<PortType, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct PortTypeVisitor {}

        impl<'de> Visitor<'de> for PortTypeVisitor {
            type Value = PortType;

            fn expecting(
                &self,
                fmt: &mut std::fmt::Formatter<'_>,
            ) -> std::result::Result<(), std::fmt::Error> {
                fmt.write_str("We expected a string")
            }

            fn visit_str<E>(self, variant: &str) -> Result<Self::Value, E> {
                Ok(variant.into())
            }

            fn visit_string<E>(self, variant: String) -> Result<Self::Value, E> {
                Ok(variant.into())
            }
        }

        de.deserialize_string(PortTypeVisitor {})
    }
}

fn main() {
    let input = r#"
    {
      "PrivatePort": 2222,
      "PublicPort": 3333,
      "Type": "dccp"
    }
    "#;

    let result: Result<PortMapping, _> = from_str(input);

    println!("{:#?}", result);
}

I don't think there is a idiomatic way to do this, that could be included in the future.

Emelyemelyne answered 13/8, 2019 at 10:9 Comment(1)
Yes there is an idiomatic way to do this that could be included in the future: #[serde(other). See github.com/serde-rs/serde/issues/912Vagina

© 2022 - 2025 — McMap. All rights reserved.