英文:
Is there a better way to derive serde impls for untagged Rust enums with unit variants?
问题
我的团队一直在努力为应该被序列化为字符串的枚举派生Deserialize
和Serialize
实现,其中包括一些预定义的字符串值以及一个"other"情况。问题的关键是,我们不知道如何在未标记的枚举中支持单元变体,而不需要大量的样板代码。
我想写一个像这样的枚举:
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BinaryComparisonOperator {
LessThan,
GreaterThan,
Equal,
Custom(String),
}
我希望它序列化为以下方式:
Rust | JSON |
---|---|
BinaryComparisonOperator::LessThan | "less_than" |
BinaryComparisonOperator::GreaterThan | "greater_than" |
BinaryComparisonOperator::Equal | "equal" |
BinaryComparisonOperator::Custom("other_op") | "other_op" |
但这不起作用。单元变体(LessThan
,GreaterThan
,Equal
)序列化如预期,但Custom
变体以以下外部标签进行序列化:
{ "custom": "other_op" } // 不是我们想要的!
我尝试在枚举上添加#[serde(untagged)]
以删除标签。这修复了Custom
情况,但然后所有单元变体都无法序列化或反序列化。所有测试都反序列化为Custom
变体,而不应该,而所有单元变体都序列化为Null
。
我们想出了这个版本,它可以工作,但比我喜欢的要复杂得多:
use serde::{de, Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[serde(untagged)]
pub enum BinaryComparisonOperator {
#[serde(deserialize_with = "parse_less_than")]
LessThan,
#[serde(deserialize_with = "parse_greater_than")]
GreaterThan,
#[serde(deserialize_with = "parse_equal")]
Equal,
Custom(String),
}
impl Serialize for BinaryComparisonOperator {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
BinaryComparisonOperator::LessThan => serializer.serialize_str("less_than"),
BinaryComparisonOperator::GreaterThan => serializer.serialize_str("greater_than"),
BinaryComparisonOperator::Equal => serializer.serialize_str("equal"),
BinaryComparisonOperator::Custom(s) => {
serializer.serialize_str(&s)
}
}
}
}
fn string_p<'de, D>(expected: String, input: String) -> Result<(), D::Error>
where
D: de::Deserializer<'de>,
{
if input == expected {
Ok(())
} else {
Err(de::Error::custom("invalid value"))
}
}
fn parse_less_than<'de, D>(deserializer: D) -> Result<(), D::Error>
where
D: de::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
string_p::<'de, D>(s, "less_than".to_owned())
}
// 还有更多的解析辅助函数...
我看到了https://github.com/serde-rs/serde/issues/1158,它似乎暗示了我们复杂的解决方案是必需的。但我希望也许有一个更好的解决方案,我们忽略了吗?
英文:
My team has been struggling to derive Deserialize
and Serialize
impls for an enum that should be serialized as a string, with some predefined string values and with an "other" case. The gist of the problem is that we don't know of a way to support unit variants in an untagged enum without lots of boilerplate.
I want to write an enum like this,
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BinaryComparisonOperator {
LessThan,
GreaterThan,
Equal,
Custom(String),
}
And I want it to serialize like this:
Rust | JSON |
---|---|
BinaryComparisonOperator::LessThan | "less_than" |
BinaryComparisonOperator::GreaterThan | "greater_than" |
BinaryComparisonOperator::Equal | "equal" |
BinaryComparisonOperator::Custom("other_op") | "other_op" |
But that doesn't work. The unit variants (LessThan
, GreaterThan
, Equal
)
serialize as expected, but the Custom
variant serializes with an external tag
like this:
{ "custom": "other_op" } // not what we want!
I tried adding #[serde(untagged)]
to the enum to remove the tag. That fixes
the Custom
case, but then all of the unit variants fail to serialize or to
deserialize. All of our tests deserialize to the Custom
variant when they
shouldn't, and all of the unit variants serialize to Null
.
We came up with this version which works, but is way more complicated than
I like:
use serde::{de, Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[serde(untagged)]
pub enum BinaryComparisonOperator {
#[serde(deserialize_with = "parse_less_than")]
LessThan,
#[serde(deserialize_with = "parse_greater_than")]
GreaterThan,
#[serde(deserialize_with = "parse_equal")]
Equal,
Custom(String),
}
impl Serialize for BinaryComparisonOperator {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
BinaryComparisonOperator::LessThan => serializer.serialize_str("less_than"),
BinaryComparisonOperator::GreaterThan => serializer.serialize_str("greater_than"),
BinaryComparisonOperator::Equal => serializer.serialize_str("equal"),
BinaryComparisonOperator::Custom(s) => {
serializer.serialize_str(&s)
}
}
}
}
fn string_p<'de, D>(expected: String, input: String) -> Result<(), D::Error>
where
D: de::Deserializer<'de>,
{
if input == expected {
Ok(())
} else {
Err(de::Error::custom("invalid value"))
}
}
fn parse_less_than<'de, D>(deserializer: D) -> Result<(), D::Error>
where
D: de::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
string_p::<'de, D>(s, "less_than".to_owned())
}
// lots more parse helpers...
I saw https://github.com/serde-rs/serde/issues/1158 which seems to imply that
our complicated solution is necessary. But I'm hoping that maybe there is
a better solution out there that we have overlooked?
答案1
得分: 2
你可以使用两个枚举。
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(untagged)]
pub enum SerdeBinCompOp {
Known(KnownOp),
Custom(String),
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum KnownOp {
LessThan,
GreaterThan,
Equal,
}
这允许你保持父枚举不带标签,同时使用子枚举的变体名称。
这有一个缺点,即如果你希望最终的枚举是扁平的,你需要拥有两个包含所有已知变体的枚举,以及转换实现。这可以通过宏来减少。
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(from = "SerdeBinCompOp", into = "SerdeBinCompOp")]
pub enum BinaryComparisonOperator {
LessThan,
GreaterThan,
Equal,
Custom(String),
}
impl From<BinaryComparisonOperator> for SerdeBinCompOp {
fn from(bco: BinaryComparisonOperator) -> Self {
match bco {
BinaryComparisonOperator::LessThan => Self::Known(KnownOp::LessThan),
BinaryComparisonOperator::GreaterThan => Self::Known(KnownOp::GreaterThan),
BinaryComparisonOperator::Equal => Self::Known(KnownOp::Equal),
BinaryComparisonOperator::Custom(s) => Self::Custom(s),
}
}
}
impl From<SerdeBinCompOp> for BinaryComparisonOperator {
fn from(sbco: SerdeBinCompOp) -> Self {
match sbco {
SerdeBinCompOp::Known(KnownOp::LessThan) => Self::LessThan,
SerdeBinCompOp::Known(KnownOp::GreaterThan) => Self::GreaterThan,
SerdeBinCompOp::Known(KnownOp::Equal) => Self::Equal,
SerdeBinCompOp::Custom(s) => Self::Custom(s),
}
}
}
英文:
You can use two enums.
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(untagged)]
pub enum SerdeBinCompOp {
Known(KnownOp),
Custom(String),
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum KnownOp {
LessThan,
GreaterThan,
Equal,
}
This allows you to leave the parent enum untagged while using the child enum's variant names.
This has the disadvantage that if you want the final enum to be flat, you need to have two enums with all the known variants, plus conversion implementations. This could be reduced with a macro.
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(from = "SerdeBinCompOp", into = "SerdeBinCompOp")]
pub enum BinaryComparisonOperator {
LessThan,
GreaterThan,
Equal,
Custom(String),
}
impl From<BinaryComparisonOperator> for SerdeBinCompOp {
fn from(bco: BinaryComparisonOperator) -> Self {
match bco {
BinaryComparisonOperator::LessThan => Self::Known(KnownOp::LessThan),
BinaryComparisonOperator::GreaterThan => Self::Known(KnownOp::GreaterThan),
BinaryComparisonOperator::Equal => Self::Known(KnownOp::Equal),
BinaryComparisonOperator::Custom(s) => Self::Custom(s),
}
}
}
impl From<SerdeBinCompOp> for BinaryComparisonOperator {
fn from(sbco: SerdeBinCompOp) -> Self {
match sbco {
SerdeBinCompOp::Known(KnownOp::LessThan) => Self::LessThan,
SerdeBinCompOp::Known(KnownOp::GreaterThan) => Self::GreaterThan,
SerdeBinCompOp::Known(KnownOp::Equal) => Self::Equal,
SerdeBinCompOp::Custom(s) => Self::Custom(s),
}
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论